From a58d689a6b9a83fa2fba0b1ee9630c83027bf4c6 Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Fri, 12 Jan 2024 14:17:25 -0600
Subject: [PATCH 01/61] add auto-width tester

---
 samples/auto_resize_width.lua | 58 +++++++++++++++++++++++++++++++++++
 1 file changed, 58 insertions(+)
 create mode 100644 samples/auto_resize_width.lua

diff --git a/samples/auto_resize_width.lua b/samples/auto_resize_width.lua
new file mode 100644
index 00000000..7f786db7
--- /dev/null
+++ b/samples/auto_resize_width.lua
@@ -0,0 +1,58 @@
+function plugindef()
+    finaleplugin.RequireDocument = false
+    finaleplugin.MinJWLuaVersion = 0.71
+    return "0--auto resize width test"
+end
+
+local mixin = require('library.mixin')
+
+--local dlg = finale.FCCustomLuaWindow()
+local dlg = mixin.FCXCustomLuaWindow()
+dlg:SetTitle(finale.FCString("Test Auto Resize Width"))
+
+local y = 0
+
+local ctrl_static = dlg:CreateStatic(0, y)
+ctrl_static:SetAutoResizeWidth(true)
+ctrl_static:SetWidth(0)
+ctrl_static:SetText(finale.FCString("Short."))
+y = y + 20
+
+local ctrl_checkbox = dlg:CreateCheckbox(0, y)
+ctrl_checkbox:SetAutoResizeWidth(true)
+ctrl_checkbox:SetWidth(0)
+ctrl_checkbox:SetText(finale.FCString("Short."))
+y = y + 20
+
+local ctrl_edit = dlg:CreateEdit(0, y)
+ctrl_edit:SetAutoResizeWidth(true)
+ctrl_edit:SetWidth(0)
+ctrl_edit:SetText(finale.FCString("Short."))
+y = y + 30
+
+local ctrl_button = dlg:CreateButton(0, 70)
+ctrl_button:SetAutoResizeWidth(true)
+ctrl_button:SetWidth(0)
+ctrl_button:SetText(finale.FCString("Short."))
+y = y + 30
+
+local ctrl_popup = dlg:CreatePopup(0, y)
+ctrl_popup:SetAutoResizeWidth(true)
+--ctrl_popup:SetWidth(0)
+for counter = 1, 3 do
+    ctrl_popup:AddString(finale.FCString("This is long option text " .. counter))
+end
+--ctrl_popup:SetText(finale.FCString("Test."))
+y = y + 20
+
+local ctrl_radiobuttons = dlg:CreateRadioButtonGroup(0, y, 3)
+local counter = 1
+for rbtn in each(ctrl_radiobuttons) do
+    rbtn:SetWidth(0)
+    rbtn:SetAutoResizeWidth(true)
+    rbtn:SetText(finale.FCString("This is long option text " .. counter))
+    counter = counter + 1
+end
+
+dlg:CreateOkButton()
+dlg:ExecuteModal(nil)

From 4f829816015b957dc8a5b2ef9b207c2d34152d31 Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Fri, 12 Jan 2024 20:21:44 -0600
Subject: [PATCH 02/61] auto resize width sample

---
 samples/auto_resize_width.lua | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/samples/auto_resize_width.lua b/samples/auto_resize_width.lua
index 7f786db7..da248059 100644
--- a/samples/auto_resize_width.lua
+++ b/samples/auto_resize_width.lua
@@ -7,7 +7,7 @@ end
 local mixin = require('library.mixin')
 
 --local dlg = finale.FCCustomLuaWindow()
-local dlg = mixin.FCXCustomLuaWindow()
+local dlg = mixin.FCMCustomWindow()
 dlg:SetTitle(finale.FCString("Test Auto Resize Width"))
 
 local y = 0
@@ -38,11 +38,11 @@ y = y + 30
 
 local ctrl_popup = dlg:CreatePopup(0, y)
 ctrl_popup:SetAutoResizeWidth(true)
---ctrl_popup:SetWidth(0)
+ctrl_popup:SetWidth(100)
 for counter = 1, 3 do
-    ctrl_popup:AddString(finale.FCString("This is long option text " .. counter))
+    ctrl_popup:AddString(finale.FCString("This is long option text " .. counter .."."))
 end
---ctrl_popup:SetText(finale.FCString("Test."))
+--ctrl_popup:SetText(finale.FCString("This is long option text 1\nThis is long option text 2\nThis is long option text 3\n"))
 y = y + 20
 
 local ctrl_radiobuttons = dlg:CreateRadioButtonGroup(0, y, 3)
@@ -50,7 +50,7 @@ local counter = 1
 for rbtn in each(ctrl_radiobuttons) do
     rbtn:SetWidth(0)
     rbtn:SetAutoResizeWidth(true)
-    rbtn:SetText(finale.FCString("This is long option text " .. counter))
+    rbtn:SetText(finale.FCString("This is long option text " .. counter .."."))
     counter = counter + 1
 end
 

From e04897afcb0090dba7346223cbecb031e7b9a40c Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Sat, 13 Jan 2024 18:50:07 -0600
Subject: [PATCH 03/61] refinements

---
 samples/auto_resize_width.lua | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/samples/auto_resize_width.lua b/samples/auto_resize_width.lua
index da248059..0cbaed36 100644
--- a/samples/auto_resize_width.lua
+++ b/samples/auto_resize_width.lua
@@ -14,7 +14,7 @@ local y = 0
 
 local ctrl_static = dlg:CreateStatic(0, y)
 ctrl_static:SetAutoResizeWidth(true)
-ctrl_static:SetWidth(0)
+ctrl_static:SetWidth(20)
 ctrl_static:SetText(finale.FCString("Short."))
 y = y + 20
 
@@ -48,7 +48,7 @@ y = y + 20
 local ctrl_radiobuttons = dlg:CreateRadioButtonGroup(0, y, 3)
 local counter = 1
 for rbtn in each(ctrl_radiobuttons) do
-    rbtn:SetWidth(0)
+    rbtn:SetWidth(20)
     rbtn:SetAutoResizeWidth(true)
     rbtn:SetText(finale.FCString("This is long option text " .. counter .."."))
     counter = counter + 1

From 34a00efe3a8dd285d331469ad03d26ba1c52ff07 Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Sun, 14 Jan 2024 15:53:16 -0600
Subject: [PATCH 04/61] add ComboBox to mixin, plus sample

---
 samples/auto_resize_width.lua | 127 +++++++++++++++++++++-------------
 1 file changed, 79 insertions(+), 48 deletions(-)

diff --git a/samples/auto_resize_width.lua b/samples/auto_resize_width.lua
index 0cbaed36..f45d51b5 100644
--- a/samples/auto_resize_width.lua
+++ b/samples/auto_resize_width.lua
@@ -6,53 +6,84 @@ end
 
 local mixin = require('library.mixin')
 
---local dlg = finale.FCCustomLuaWindow()
-local dlg = mixin.FCMCustomWindow()
-dlg:SetTitle(finale.FCString("Test Auto Resize Width"))
-
-local y = 0
-
-local ctrl_static = dlg:CreateStatic(0, y)
-ctrl_static:SetAutoResizeWidth(true)
-ctrl_static:SetWidth(20)
-ctrl_static:SetText(finale.FCString("Short."))
-y = y + 20
-
-local ctrl_checkbox = dlg:CreateCheckbox(0, y)
-ctrl_checkbox:SetAutoResizeWidth(true)
-ctrl_checkbox:SetWidth(0)
-ctrl_checkbox:SetText(finale.FCString("Short."))
-y = y + 20
-
-local ctrl_edit = dlg:CreateEdit(0, y)
-ctrl_edit:SetAutoResizeWidth(true)
-ctrl_edit:SetWidth(0)
-ctrl_edit:SetText(finale.FCString("Short."))
-y = y + 30
-
-local ctrl_button = dlg:CreateButton(0, 70)
-ctrl_button:SetAutoResizeWidth(true)
-ctrl_button:SetWidth(0)
-ctrl_button:SetText(finale.FCString("Short."))
-y = y + 30
-
-local ctrl_popup = dlg:CreatePopup(0, y)
-ctrl_popup:SetAutoResizeWidth(true)
-ctrl_popup:SetWidth(100)
-for counter = 1, 3 do
-    ctrl_popup:AddString(finale.FCString("This is long option text " .. counter .."."))
-end
---ctrl_popup:SetText(finale.FCString("This is long option text 1\nThis is long option text 2\nThis is long option text 3\n"))
-y = y + 20
-
-local ctrl_radiobuttons = dlg:CreateRadioButtonGroup(0, y, 3)
-local counter = 1
-for rbtn in each(ctrl_radiobuttons) do
-    rbtn:SetWidth(20)
-    rbtn:SetAutoResizeWidth(true)
-    rbtn:SetText(finale.FCString("This is long option text " .. counter .."."))
-    counter = counter + 1
+function create_dialog()
+    --local dlg = finale.FCCustomLuaWindow()
+    local dlg = mixin.FCXCustomLuaWindow()
+    dlg:SetTitle(finale.FCString("Test Auto Resize Width"))
+
+    local y = 0
+
+    local ctrl_static = dlg:CreateStatic(0, y)
+    ctrl_static:SetAutoResizeWidth(true)
+    ctrl_static:SetWidth(20)
+    ctrl_static:SetText(finale.FCString("This is long option text."))
+    y = y + 20
+
+    local ctrl_checkbox = dlg:CreateCheckbox(0, y)
+    ctrl_checkbox:SetAutoResizeWidth(true)
+    ctrl_checkbox:SetWidth(0)
+    ctrl_checkbox:SetText(finale.FCString("Short."))
+    y = y + 20
+
+    local ctrl_edit = dlg:CreateEdit(0, y)
+    ctrl_edit:SetAutoResizeWidth(true)
+    ctrl_edit:SetWidth(0)
+    ctrl_edit:SetText(finale.FCString("Short."))
+    --ctrl_edit:SetMeasurement(1, finale.MEASUREMENTUNIT_DEFAULT)
+    y = y + 30
+
+    local ctrl_button = dlg:CreateButton(0, 70)
+    ctrl_button:SetAutoResizeWidth(true)
+    ctrl_button:SetWidth(0)
+    ctrl_button:SetText(finale.FCString("Short."))
+    y = y + 30
+
+    local ctrl_popup = dlg:CreatePopup(0, y)
+    ctrl_popup:SetAutoResizeWidth(true)
+    ctrl_popup:SetWidth(0)
+    for counter = 1, 3 do
+        if counter == 3 then
+            ctrl_popup:AddString(finale.FCString("This is long option text " .. counter .. "."))
+        else
+            ctrl_popup:AddString(finale.FCString("Short " .. counter .. "."))
+        end
+    end
+    ctrl_popup:SetSelectedItem(2)
+    y = y + 20
+
+    local ctrl_cbobox = dlg:CreateComboBox(0, y)
+    ctrl_cbobox:SetAutoResizeWidth(true)
+    ctrl_cbobox:SetWidth(40)
+    for counter = 1, 3 do
+        if counter == 3 then
+            ctrl_cbobox:AddString(finale.FCString("This is long option text " .. counter .. "."))
+        else
+            ctrl_cbobox:AddString(finale.FCString("Short " .. counter .. "."))
+        end
+    end
+    ctrl_cbobox:SetSelectedItem(2)
+    y = y + 20
+
+        --[[
+    local ctrl_radiobuttons = dlg:CreateRadioButtonGroup(0, y, 3)
+    local counter = 1
+    for rbtn in each(ctrl_radiobuttons) do
+        rbtn:SetWidth(20)
+        rbtn:SetAutoResizeWidth(true)
+        --rbtn:SetText(finale.FCString("This is long option text " .. counter .. "."))
+        rbtn:SetText(finale.FCString("Short " .. counter .. "."))
+        counter = counter + 1
+    end
+]]
+
+    dlg:RegisterInitWindow(function()
+        --ctrl_edit:SetMeasurement(1, finale.MEASUREMENTUNIT_DEFAULT)
+    end)
+
+    dlg:CreateOkButton()
+
+    return dlg
 end
 
-dlg:CreateOkButton()
-dlg:ExecuteModal(nil)
+global_dialog = global_dialog or create_dialog()
+global_dialog:RunModeless()

From 8d366dcea323068db3e2ec8363bfceb6b635dc1d Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Sun, 14 Jan 2024 21:14:52 -0600
Subject: [PATCH 05/61] lint fix and sample update

---
 samples/auto_resize_width.lua | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/samples/auto_resize_width.lua b/samples/auto_resize_width.lua
index f45d51b5..7cc1bd71 100644
--- a/samples/auto_resize_width.lua
+++ b/samples/auto_resize_width.lua
@@ -64,7 +64,7 @@ function create_dialog()
     ctrl_cbobox:SetSelectedItem(2)
     y = y + 20
 
-        --[[
+    --[[
     local ctrl_radiobuttons = dlg:CreateRadioButtonGroup(0, y, 3)
     local counter = 1
     for rbtn in each(ctrl_radiobuttons) do
@@ -75,7 +75,6 @@ function create_dialog()
         counter = counter + 1
     end
 ]]
-
     dlg:RegisterInitWindow(function()
         --ctrl_edit:SetMeasurement(1, finale.MEASUREMENTUNIT_DEFAULT)
     end)

From 7525a9f863a525e634a51fbf0dafff415cfb0ecc Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Tue, 16 Jan 2024 14:08:59 -0600
Subject: [PATCH 06/61] add other auto-layout features

---
 ...{auto_resize_width.lua => auto_layout.lua} | 35 ++++++++++++-------
 1 file changed, 23 insertions(+), 12 deletions(-)
 rename samples/{auto_resize_width.lua => auto_layout.lua} (73%)

diff --git a/samples/auto_resize_width.lua b/samples/auto_layout.lua
similarity index 73%
rename from samples/auto_resize_width.lua
rename to samples/auto_layout.lua
index 7cc1bd71..3299b11c 100644
--- a/samples/auto_resize_width.lua
+++ b/samples/auto_layout.lua
@@ -6,17 +6,25 @@ end
 
 local mixin = require('library.mixin')
 
+local function win_mac(winval, macval)
+    if finenv.UI():IsOnWindows() then return winval end
+    return macval
+end
+
 function create_dialog()
     --local dlg = finale.FCCustomLuaWindow()
     local dlg = mixin.FCXCustomLuaWindow()
-    dlg:SetTitle(finale.FCString("Test Auto Resize Width"))
+    dlg:SetTitle(finale.FCString("Test Autolayout"))
 
     local y = 0
 
-    local ctrl_static = dlg:CreateStatic(0, y)
-    ctrl_static:SetAutoResizeWidth(true)
-    ctrl_static:SetWidth(20)
-    ctrl_static:SetText(finale.FCString("This is long option text."))
+    local ctrl_label = dlg:CreateStatic(0, y)
+    ctrl_label.AutoResizeWidth = true
+    ctrl_label:SetWidth(0)
+    ctrl_label:SetText(finale.FCString("Label:"))
+    local ctrl_edit = dlg:CreateEdit(0, y - win_mac(5, 3))
+    ctrl_edit:SetText("Editable")
+    ctrl_edit:AssureNoHorizontalOverlap(ctrl_label, 0)
     y = y + 20
 
     local ctrl_checkbox = dlg:CreateCheckbox(0, y)
@@ -62,24 +70,27 @@ function create_dialog()
         end
     end
     ctrl_cbobox:SetSelectedItem(2)
-    y = y + 20
+    y = y + 30
 
-    --[[
     local ctrl_radiobuttons = dlg:CreateRadioButtonGroup(0, y, 3)
     local counter = 1
     for rbtn in each(ctrl_radiobuttons) do
-        rbtn:SetWidth(20)
-        rbtn:SetAutoResizeWidth(true)
-        --rbtn:SetText(finale.FCString("This is long option text " .. counter .. "."))
-        rbtn:SetText(finale.FCString("Short " .. counter .. "."))
+        rbtn:SetWidth(0)
+        rbtn.AutoResizeWidth = true
+        if counter == 2 then
+            rbtn:SetText(finale.FCString("This is long option text " .. counter .. "."))
+        else
+            rbtn:SetText(finale.FCString("Short " .. counter .. "."))
+        end
         counter = counter + 1
     end
-]]
+
     dlg:RegisterInitWindow(function()
         --ctrl_edit:SetMeasurement(1, finale.MEASUREMENTUNIT_DEFAULT)
     end)
 
     dlg:CreateOkButton()
+    dlg:CreateCancelButton()
 
     return dlg
 end

From bf6cdb32c5946a16914efe6076190306b9b12a05 Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Tue, 16 Jan 2024 21:03:49 -0600
Subject: [PATCH 07/61] sample auto_layout updates

---
 samples/auto_layout.lua | 55 +++++++++++++++++++++++++++--------------
 1 file changed, 37 insertions(+), 18 deletions(-)

diff --git a/samples/auto_layout.lua b/samples/auto_layout.lua
index 3299b11c..10e2ea13 100644
--- a/samples/auto_layout.lua
+++ b/samples/auto_layout.lua
@@ -19,36 +19,45 @@ function create_dialog()
     local y = 0
 
     local ctrl_label = dlg:CreateStatic(0, y)
-    ctrl_label.AutoResizeWidth = true
+    ctrl_label:DoAutoResizeWidth(true)
     ctrl_label:SetWidth(0)
     ctrl_label:SetText(finale.FCString("Label:"))
-    local ctrl_edit = dlg:CreateEdit(0, y - win_mac(5, 3))
-    ctrl_edit:SetText("Editable")
-    ctrl_edit:AssureNoHorizontalOverlap(ctrl_label, 0)
+    local ctrl_edit = dlg:CreateEdit(0, y - win_mac(2, 3))
+    ctrl_edit:SetText(finale.FCString("Editable"))
+    ctrl_edit:AssureNoHorizontalOverlap(ctrl_label, 2)
     y = y + 20
 
     local ctrl_checkbox = dlg:CreateCheckbox(0, y)
-    ctrl_checkbox:SetAutoResizeWidth(true)
+    ctrl_checkbox:DoAutoResizeWidth(true)
     ctrl_checkbox:SetWidth(0)
     ctrl_checkbox:SetText(finale.FCString("Short."))
     y = y + 20
 
-    local ctrl_edit = dlg:CreateEdit(0, y)
-    ctrl_edit:SetAutoResizeWidth(true)
-    ctrl_edit:SetWidth(0)
-    ctrl_edit:SetText(finale.FCString("Short."))
-    --ctrl_edit:SetMeasurement(1, finale.MEASUREMENTUNIT_DEFAULT)
+    local ctrl_edit2 = dlg:CreateEdit(0, y)
+    ctrl_edit2:DoAutoResizeWidth(true)
+    ctrl_edit2:SetWidth(0)
+    ctrl_edit2:SetText(finale.FCString("Short."))
+    --ctrl_edit2:SetMeasurement(1, finale.MEASUREMENTUNIT_DEFAULT)
     y = y + 30
 
     local ctrl_button = dlg:CreateButton(0, 70)
-    ctrl_button:SetAutoResizeWidth(true)
+    ctrl_button:DoAutoResizeWidth(true)
     ctrl_button:SetWidth(0)
     ctrl_button:SetText(finale.FCString("Short."))
     y = y + 30
 
-    local ctrl_popup = dlg:CreatePopup(0, y)
-    ctrl_popup:SetAutoResizeWidth(true)
+    local popup_label = dlg:CreateStatic(0, y)
+    popup_label:DoAutoResizeWidth(true)
+    popup_label:SetWidth(0)
+    popup_label:SetText(finale.FCString("Popup:"))
+    local ctrl_popup = dlg:CreatePopup(0, y - win_mac(2, 2))
+    ctrl_popup:DoAutoResizeWidth(true)
     ctrl_popup:SetWidth(0)
+    ctrl_popup:AssureNoHorizontalOverlap(popup_label, 2)
+    --ctrl_popup:HorizontallyAlignWith(ctrl_edit)
+    for k, v in pairs(finale.FCControl.__propget) do
+        print (tostring(k), tostring(v), tostring(ctrl_popup[k]))
+    end
     for counter = 1, 3 do
         if counter == 3 then
             ctrl_popup:AddString(finale.FCString("This is long option text " .. counter .. "."))
@@ -56,12 +65,21 @@ function create_dialog()
             ctrl_popup:AddString(finale.FCString("Short " .. counter .. "."))
         end
     end
-    ctrl_popup:SetSelectedItem(2)
-    y = y + 20
+    ctrl_popup:SetSelectedItem(1)
+    y = y + 22
 
-    local ctrl_cbobox = dlg:CreateComboBox(0, y)
-    ctrl_cbobox:SetAutoResizeWidth(true)
+    local cbobox_label = dlg:CreateStatic(0, y)
+    cbobox_label:DoAutoResizeWidth(true)
+    cbobox_label:SetWidth(0)
+    cbobox_label:SetText(finale.FCString("ComboBox:"))
+    local ctrl_cbobox = dlg:CreateComboBox(0, y - win_mac(2, 4))
+    ctrl_cbobox:DoAutoResizeWidth(true)
     ctrl_cbobox:SetWidth(40)
+    ctrl_cbobox:AssureNoHorizontalOverlap(cbobox_label, 2)
+    ctrl_cbobox:HorizontallyAlignWith(ctrl_popup)
+    for k, v in pairs(finale.FCControl.__propget) do
+        print (tostring(k), tostring(v), tostring(ctrl_cbobox[k]))
+    end
     for counter = 1, 3 do
         if counter == 3 then
             ctrl_cbobox:AddString(finale.FCString("This is long option text " .. counter .. "."))
@@ -76,7 +94,7 @@ function create_dialog()
     local counter = 1
     for rbtn in each(ctrl_radiobuttons) do
         rbtn:SetWidth(0)
-        rbtn.AutoResizeWidth = true
+        rbtn:DoAutoResizeWidth(true)
         if counter == 2 then
             rbtn:SetText(finale.FCString("This is long option text " .. counter .. "."))
         else
@@ -97,3 +115,4 @@ end
 
 global_dialog = global_dialog or create_dialog()
 global_dialog:RunModeless()
+--global_dialog:ExecuteModal(nil)

From 2ff3ea6a8668c26b6a5b5b16cd1628bae6be360c Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Wed, 17 Jan 2024 09:56:40 -0600
Subject: [PATCH 08/61] auto-layout sample for 0.71

---
 .vscode/recommended_settings.json |   7 +-
 samples/auto_layout.lua           | 187 +++++++++++++++++++-----------
 src/library/utils.lua             |  17 +++
 3 files changed, 139 insertions(+), 72 deletions(-)

diff --git a/.vscode/recommended_settings.json b/.vscode/recommended_settings.json
index 3559e256..0f1050f7 100644
--- a/.vscode/recommended_settings.json
+++ b/.vscode/recommended_settings.json
@@ -14,18 +14,19 @@
         "loadall",
         "loadallforregion",
         "pairsbykeys",
+        "prettyformatjson",
         "bit32",
         "utf8",
         "socket",
         "tinyxml2",
         "xmlelements",
-        "xmlattributes",
-        "prettyformatjson"
+        "xmlattributes"
     ],
     "files.exclude": {
         "decoder.lua": true,
         "mobdebug.lua": true,
-        "sax.lua": true
+        "sax.lua": true,
+        "dist/**": true
     },
     "editor.formatOnSave": true,
     "Lua.diagnostics.enable": false,
diff --git a/samples/auto_layout.lua b/samples/auto_layout.lua
index 10e2ea13..9acd5173 100644
--- a/samples/auto_layout.lua
+++ b/samples/auto_layout.lua
@@ -4,101 +4,150 @@ function plugindef()
     return "0--auto resize width test"
 end
 
+local utils = require('library.utils')
 local mixin = require('library.mixin')
 
-local function win_mac(winval, macval)
-    if finenv.UI():IsOnWindows() then return winval end
-    return macval
-end
-
 function create_dialog()
-    --local dlg = finale.FCCustomLuaWindow()
     local dlg = mixin.FCXCustomLuaWindow()
     dlg:SetTitle(finale.FCString("Test Autolayout"))
 
     local y = 0
+    local line_no = 0
+    local y_increment = 22
+    local label_edit_separ = 3
+    local center_padding = 20
 
-    local ctrl_label = dlg:CreateStatic(0, y)
-    ctrl_label:DoAutoResizeWidth(true)
-    ctrl_label:SetWidth(0)
-    ctrl_label:SetText(finale.FCString("Label:"))
-    local ctrl_edit = dlg:CreateEdit(0, y - win_mac(2, 3))
-    ctrl_edit:SetText(finale.FCString("Editable"))
-    ctrl_edit:AssureNoHorizontalOverlap(ctrl_label, 2)
-    y = y + 20
-
-    local ctrl_checkbox = dlg:CreateCheckbox(0, y)
-    ctrl_checkbox:DoAutoResizeWidth(true)
-    ctrl_checkbox:SetWidth(0)
-    ctrl_checkbox:SetText(finale.FCString("Short."))
-    y = y + 20
-
-    local ctrl_edit2 = dlg:CreateEdit(0, y)
-    ctrl_edit2:DoAutoResizeWidth(true)
-    ctrl_edit2:SetWidth(0)
-    ctrl_edit2:SetText(finale.FCString("Short."))
-    --ctrl_edit2:SetMeasurement(1, finale.MEASUREMENTUNIT_DEFAULT)
-    y = y + 30
-
-    local ctrl_button = dlg:CreateButton(0, 70)
-    ctrl_button:DoAutoResizeWidth(true)
-    ctrl_button:SetWidth(0)
-    ctrl_button:SetText(finale.FCString("Short."))
-    y = y + 30
-
-    local popup_label = dlg:CreateStatic(0, y)
-    popup_label:DoAutoResizeWidth(true)
-    popup_label:SetWidth(0)
-    popup_label:SetText(finale.FCString("Popup:"))
-    local ctrl_popup = dlg:CreatePopup(0, y - win_mac(2, 2))
-    ctrl_popup:DoAutoResizeWidth(true)
-    ctrl_popup:SetWidth(0)
-    ctrl_popup:AssureNoHorizontalOverlap(popup_label, 2)
-    --ctrl_popup:HorizontallyAlignWith(ctrl_edit)
-    for k, v in pairs(finale.FCControl.__propget) do
-        print (tostring(k), tostring(v), tostring(ctrl_popup[k]))
-    end
+    -- left side
+    dlg:CreateStatic(0, line_no * y_increment, "option1-label")
+        :DoAutoResizeWidth(true)
+        :SetWidth(0)
+        :SetText("First Option:")
+    dlg:CreateEdit(0, line_no * y_increment - utils.win_mac(2, 3), "option1")
+        :SetInteger(1)
+        :AssureNoHorizontalOverlap(dlg:GetControl("option1-label"), label_edit_separ)
+    line_no = line_no + 1
+
+    dlg:CreateCheckbox(0, line_no * y_increment, "left-checkbox1")
+        :DoAutoResizeWidth(true)
+        :SetWidth(0)
+        :SetText("Left Checkbox Option 1")
+    line_no = line_no + 1
+
+    dlg:CreateStatic(0, line_no * y_increment, "option2-label")
+        :DoAutoResizeWidth(true)
+        :SetWidth(0)
+        :SetText("Second Option:")
+    dlg:CreateEdit(0, line_no * y_increment - utils.win_mac(2, 3), "option2")
+        :SetInteger(2)
+        :AssureNoHorizontalOverlap(dlg:GetControl("option2-label"), label_edit_separ)
+        :HorizontallyAlignWith(dlg:GetControl("option1"))
+    line_no = line_no + 1
+
+    dlg:CreateCheckbox(0, line_no * y_increment, "left-checkbox2")
+        :DoAutoResizeWidth(true)
+        :SetWidth(0)
+        :SetText("Left Checkbox Option 2")
+    line_no = line_no + 1
+
+    -- center line
+    local vertical_line= dlg:CreateVerticalLine(0, 0 - utils.win_mac(2, 3), line_no * y_increment)
+        :AssureNoHorizontalOverlap(dlg:GetControl("option1"), center_padding)
+        :AssureNoHorizontalOverlap(dlg:GetControl("left-checkbox1"), center_padding)
+        :AssureNoHorizontalOverlap(dlg:GetControl("option2"), center_padding)
+        :AssureNoHorizontalOverlap(dlg:GetControl("left-checkbox2"), center_padding)
+    line_no = 0
+
+    -- right side
+    dlg:CreateStatic(0, line_no * y_increment, "option3-label")
+        :DoAutoResizeWidth(true)
+        :SetWidth(0)
+        :SetText("Third Option:")
+        :AssureNoHorizontalOverlap(vertical_line, center_padding)
+    dlg:CreateEdit(0, line_no * y_increment - utils.win_mac(2, 3), "option3")
+        :SetInteger(3)
+        :AssureNoHorizontalOverlap(dlg:GetControl("option3-label"), label_edit_separ)
+    line_no = line_no + 1
+
+    dlg:CreateCheckbox(0, line_no * y_increment, "right-checkbox1")
+        :DoAutoResizeWidth(true)
+        :SetWidth(0)
+        :SetText("Right Checkbox Option 1")
+        :AssureNoHorizontalOverlap(vertical_line, center_padding)
+    line_no = line_no + 1
+
+    dlg:CreateStatic(0, line_no * y_increment, "option4-label")
+        :DoAutoResizeWidth(true)
+        :SetWidth(0)
+        :SetText("Fourth Option:")
+        :AssureNoHorizontalOverlap(vertical_line, center_padding)
+    dlg:CreateEdit(0, line_no * y_increment - utils.win_mac(2, 3), "option4")
+        :SetInteger(4)
+        :AssureNoHorizontalOverlap(dlg:GetControl("option4-label"), label_edit_separ)
+        :HorizontallyAlignWith(dlg:GetControl("option3"))
+    line_no = line_no + 1
+
+    dlg:CreateButton(0, line_no * y_increment)
+        :DoAutoResizeWidth(true)
+        :SetWidth(0)
+        :SetText("Action Button")
+        :AssureNoHorizontalOverlap(vertical_line, center_padding)
+        :HorizontallyAlignWith(dlg:GetControl("option4"), true)
+    line_no = line_no + 1
+
+    -- horizontal line here
+    line_no = line_no + 1
+
+    -- bottom side
+    local start_line_no = line_no
+    dlg:CreateStatic(0, line_no * y_increment, "popup_label")
+        :DoAutoResizeWidth(true)
+        :SetWidth(0)
+        :SetText("Menu:")
+    local ctrl_popup = dlg:CreatePopup(0, line_no * y_increment - utils.win_mac(2, 2), "popup")
+        :DoAutoResizeWidth(true)
+        :SetWidth(0)
+        :AssureNoHorizontalOverlap(dlg:GetControl("popup_label"), label_edit_separ)
     for counter = 1, 3 do
         if counter == 3 then
-            ctrl_popup:AddString(finale.FCString("This is long option text " .. counter .. "."))
+            ctrl_popup:AddString(finale.FCString("This is long menu text " .. counter .. "."))
         else
             ctrl_popup:AddString(finale.FCString("Short " .. counter .. "."))
         end
     end
-    ctrl_popup:SetSelectedItem(1)
-    y = y + 22
-
-    local cbobox_label = dlg:CreateStatic(0, y)
-    cbobox_label:DoAutoResizeWidth(true)
-    cbobox_label:SetWidth(0)
-    cbobox_label:SetText(finale.FCString("ComboBox:"))
-    local ctrl_cbobox = dlg:CreateComboBox(0, y - win_mac(2, 4))
-    ctrl_cbobox:DoAutoResizeWidth(true)
-    ctrl_cbobox:SetWidth(40)
-    ctrl_cbobox:AssureNoHorizontalOverlap(cbobox_label, 2)
-    ctrl_cbobox:HorizontallyAlignWith(ctrl_popup)
-    for k, v in pairs(finale.FCControl.__propget) do
-        print (tostring(k), tostring(v), tostring(ctrl_cbobox[k]))
-    end
+    ctrl_popup:SetSelectedItem(0)
+    line_no = line_no + 1
+
+    dlg:CreateStatic(0, line_no * y_increment, "cbobox_label")
+        :DoAutoResizeWidth(true)
+        :SetWidth(0)
+        :SetText("Choices:")
+    local ctrl_cbobox = dlg:CreateComboBox(0, line_no * y_increment - utils.win_mac(2, 4), "cbobox")
+        :DoAutoResizeWidth(true)
+        :SetWidth(40)
+        :AssureNoHorizontalOverlap(dlg:GetControl("cbobox_label"), label_edit_separ)
+        :HorizontallyAlignWith(ctrl_popup)
     for counter = 1, 3 do
         if counter == 3 then
-            ctrl_cbobox:AddString(finale.FCString("This is long option text " .. counter .. "."))
+            ctrl_cbobox:AddString(finale.FCString("This is long text choice " .. counter .. "."))
         else
             ctrl_cbobox:AddString(finale.FCString("Short " .. counter .. "."))
         end
     end
-    ctrl_cbobox:SetSelectedItem(2)
-    y = y + 30
+    ctrl_cbobox:SetSelectedItem(0)
+    line_no = line_no + 1
 
-    local ctrl_radiobuttons = dlg:CreateRadioButtonGroup(0, y, 3)
+    line_no = start_line_no
+    local ctrl_radiobuttons = dlg:CreateRadioButtonGroup(0, line_no * y_increment, 3)
     local counter = 1
     for rbtn in each(ctrl_radiobuttons) do
         rbtn:SetWidth(0)
-        rbtn:DoAutoResizeWidth(true)
+            :DoAutoResizeWidth(true)
+            :AssureNoHorizontalOverlap(ctrl_popup, 10)
+            :AssureNoHorizontalOverlap(ctrl_cbobox, 10)
         if counter == 2 then
-            rbtn:SetText(finale.FCString("This is long option text " .. counter .. "."))
+            rbtn:SetText(finale.FCString("This is longer option text " .. counter))
         else
-            rbtn:SetText(finale.FCString("Short " .. counter .. "."))
+            rbtn:SetText(finale.FCString("Short " .. counter))
         end
         counter = counter + 1
     end
diff --git a/src/library/utils.lua b/src/library/utils.lua
index d7a35959..fa485e07 100644
--- a/src/library/utils.lua
+++ b/src/library/utils.lua
@@ -360,4 +360,21 @@ function utils.show_notes_dialog(caption, width, height)
     dlg:ExecuteModal(nil)
 end
 
+--[[
+% win_mac
+
+Returns the winval or the macval depending on which operating system the script is running on.
+
+@ windows_value (any) The Windows value to return
+@ mac_value (any) The macOS value to return
+: (any) The windows_value or mac_value based on finenv.UI()IsOnWindows()
+]]
+
+function utils.win_mac(windows_value, mac_value)
+    if finenv.UI():IsOnWindows() then
+        return windows_value
+    end
+    return mac_value
+end
+
 return utils

From aeb97b400a74f94e4d46563e2a5bdf17a9eac03d Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Wed, 17 Jan 2024 18:32:25 -0600
Subject: [PATCH 09/61] revise auto-layout demo script

---
 samples/auto_layout.lua | 30 +++++++++++++++---------------
 1 file changed, 15 insertions(+), 15 deletions(-)

diff --git a/samples/auto_layout.lua b/samples/auto_layout.lua
index 9acd5173..c8f8e625 100644
--- a/samples/auto_layout.lua
+++ b/samples/auto_layout.lua
@@ -40,7 +40,7 @@ function create_dialog()
     dlg:CreateEdit(0, line_no * y_increment - utils.win_mac(2, 3), "option2")
         :SetInteger(2)
         :AssureNoHorizontalOverlap(dlg:GetControl("option2-label"), label_edit_separ)
-        :HorizontallyAlignWith(dlg:GetControl("option1"))
+        :HorizontallyAlignLeftWith(dlg:GetControl("option1"))
     line_no = line_no + 1
 
     dlg:CreateCheckbox(0, line_no * y_increment, "left-checkbox2")
@@ -49,7 +49,7 @@ function create_dialog()
         :SetText("Left Checkbox Option 2")
     line_no = line_no + 1
 
-    -- center line
+    -- center vertical line
     local vertical_line= dlg:CreateVerticalLine(0, 0 - utils.win_mac(2, 3), line_no * y_increment)
         :AssureNoHorizontalOverlap(dlg:GetControl("option1"), center_padding)
         :AssureNoHorizontalOverlap(dlg:GetControl("left-checkbox1"), center_padding)
@@ -83,7 +83,7 @@ function create_dialog()
     dlg:CreateEdit(0, line_no * y_increment - utils.win_mac(2, 3), "option4")
         :SetInteger(4)
         :AssureNoHorizontalOverlap(dlg:GetControl("option4-label"), label_edit_separ)
-        :HorizontallyAlignWith(dlg:GetControl("option3"))
+        :HorizontallyAlignLeftWith(dlg:GetControl("option3"))
     line_no = line_no + 1
 
     dlg:CreateButton(0, line_no * y_increment)
@@ -91,10 +91,12 @@ function create_dialog()
         :SetWidth(0)
         :SetText("Action Button")
         :AssureNoHorizontalOverlap(vertical_line, center_padding)
-        :HorizontallyAlignWith(dlg:GetControl("option4"), true)
+        :HorizontallyAlignRightWith(dlg:GetControl("option4"))
     line_no = line_no + 1
 
     -- horizontal line here
+    dlg:CreateHorizontalLine(0, line_no * y_increment + utils.win_mac(7, 5), 20)
+        :StretchToAlignWithRight()
     line_no = line_no + 1
 
     -- bottom side
@@ -109,9 +111,9 @@ function create_dialog()
         :AssureNoHorizontalOverlap(dlg:GetControl("popup_label"), label_edit_separ)
     for counter = 1, 3 do
         if counter == 3 then
-            ctrl_popup:AddString(finale.FCString("This is long menu text " .. counter .. "."))
+            ctrl_popup:AddString(finale.FCString("This is long menu text " .. counter))
         else
-            ctrl_popup:AddString(finale.FCString("Short " .. counter .. "."))
+            ctrl_popup:AddString(finale.FCString("Short " .. counter))
         end
     end
     ctrl_popup:SetSelectedItem(0)
@@ -125,12 +127,12 @@ function create_dialog()
         :DoAutoResizeWidth(true)
         :SetWidth(40)
         :AssureNoHorizontalOverlap(dlg:GetControl("cbobox_label"), label_edit_separ)
-        :HorizontallyAlignWith(ctrl_popup)
+        :HorizontallyAlignLeftWith(ctrl_popup)
     for counter = 1, 3 do
         if counter == 3 then
-            ctrl_cbobox:AddString(finale.FCString("This is long text choice " .. counter .. "."))
+            ctrl_cbobox:AddString(finale.FCString("This is long text choice " .. counter))
         else
-            ctrl_cbobox:AddString(finale.FCString("Short " .. counter .. "."))
+            ctrl_cbobox:AddString(finale.FCString("Short " .. counter))
         end
     end
     ctrl_cbobox:SetSelectedItem(0)
@@ -151,13 +153,11 @@ function create_dialog()
         end
         counter = counter + 1
     end
+    line_no = line_no + 2
 
-    dlg:RegisterInitWindow(function()
-        --ctrl_edit:SetMeasurement(1, finale.MEASUREMENTUNIT_DEFAULT)
-    end)
-
-    dlg:CreateOkButton()
-    dlg:CreateCancelButton()
+    dlg:CreateCloseButton(0, line_no * y_increment + 5)
+        :HorizontallyAlignRightWithFurthest()
+        :DoAutoResizeWidth()
 
     return dlg
 end

From a2fce806f675f62791327e8203548405d5ee9e14 Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Thu, 18 Jan 2024 08:09:39 -0600
Subject: [PATCH 10/61] fix issue with FCMCtrlCheckBox not storing state

---
 samples/auto_layout.lua | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/samples/auto_layout.lua b/samples/auto_layout.lua
index c8f8e625..a3fdbded 100644
--- a/samples/auto_layout.lua
+++ b/samples/auto_layout.lua
@@ -123,7 +123,7 @@ function create_dialog()
         :DoAutoResizeWidth(true)
         :SetWidth(0)
         :SetText("Choices:")
-    local ctrl_cbobox = dlg:CreateComboBox(0, line_no * y_increment - utils.win_mac(2, 4), "cbobox")
+    local ctrl_cbobox = dlg:CreateComboBox(0, line_no * y_increment - utils.win_mac(2, 3), "cbobox")
         :DoAutoResizeWidth(true)
         :SetWidth(40)
         :AssureNoHorizontalOverlap(dlg:GetControl("cbobox_label"), label_edit_separ)

From c21a6f6ce98def4edc47796d024fd05eb609bc82 Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Thu, 18 Jan 2024 08:15:27 -0600
Subject: [PATCH 11/61] add 3-state checkbox for testing

---
 samples/auto_layout.lua | 1 +
 1 file changed, 1 insertion(+)

diff --git a/samples/auto_layout.lua b/samples/auto_layout.lua
index a3fdbded..becff967 100644
--- a/samples/auto_layout.lua
+++ b/samples/auto_layout.lua
@@ -72,6 +72,7 @@ function create_dialog()
         :DoAutoResizeWidth(true)
         :SetWidth(0)
         :SetText("Right Checkbox Option 1")
+        :SetThreeStatesMode(true)
         :AssureNoHorizontalOverlap(vertical_line, center_padding)
     line_no = line_no + 1
 

From fca08c9cc9a3d464f6fb51a891d5682d0331176e Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Thu, 18 Jan 2024 08:19:27 -0600
Subject: [PATCH 12/61] fix text

---
 samples/auto_layout.lua | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/samples/auto_layout.lua b/samples/auto_layout.lua
index becff967..16b1ee15 100644
--- a/samples/auto_layout.lua
+++ b/samples/auto_layout.lua
@@ -71,7 +71,7 @@ function create_dialog()
     dlg:CreateCheckbox(0, line_no * y_increment, "right-checkbox1")
         :DoAutoResizeWidth(true)
         :SetWidth(0)
-        :SetText("Right Checkbox Option 1")
+        :SetText("Right Three-State Option")
         :SetThreeStatesMode(true)
         :AssureNoHorizontalOverlap(vertical_line, center_padding)
     line_no = line_no + 1

From 78172fd5795df7fc1a0953fa08f7137f8027a986 Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Thu, 18 Jan 2024 17:22:04 -0600
Subject: [PATCH 13/61] add localization demo

---
 samples/auto_layout.lua                   | 197 ++++++++++++++++++++--
 samples/auto_layout_localizing_script.lua |  31 ++++
 src/library/client.lua                    |  34 +++-
 src/library/general_library.lua           |  24 ++-
 src/library/localization.lua              |  81 +++++++++
 src/library/localization_developer.lua    | 140 +++++++++++++++
 src/library/tie.lua                       |   4 +-
 7 files changed, 486 insertions(+), 25 deletions(-)
 create mode 100644 samples/auto_layout_localizing_script.lua
 create mode 100644 src/library/localization.lua
 create mode 100644 src/library/localization_developer.lua

diff --git a/samples/auto_layout.lua b/samples/auto_layout.lua
index 16b1ee15..7f067d0c 100644
--- a/samples/auto_layout.lua
+++ b/samples/auto_layout.lua
@@ -6,10 +6,173 @@ end
 
 local utils = require('library.utils')
 local mixin = require('library.mixin')
+local localization = require('library.localization')
+
+--
+-- This table was auto-generated with localization_developer.create_localized_base_table_string(en)
+-- Then it was edited to include only the strings that need to be localized.
+--
+localization_en =
+{
+    ["Action Button"] = "Action Button",
+    ["Choices"] = "Choices",
+    ["First Option"] = "First Option",
+    ["Fourth Option"] = "Fourth Option",
+    ["Left Checkbox Option 1"] = "Left Checkbox Option 1",
+    ["Left Checkbox Option 2"] = "Left Checkbox Option 2",
+    ["Menu"] = "Menu",
+    ["Right Three-State Option"] = "Right Three-State Option",
+    ["Second Option"] = "Second Option",
+    ["Short "] = "Short ",
+    ["Test Autolayout"] = "Test Autolayout",
+    ["Third Option"] = "Third Option",
+    ["This is long menu text "] = "This is long menu text ",
+    ["This is long text choice "] = "This is long text choice ",
+    ["This is longer option text "] = "This is longer option text ",
+}
+
+--
+-- The rest of the localization tables were created one-at-a-time with the auto_layout_localizing_script.lua
+--
+-- This table was auto-generated with localization_developer.translate_localized_table_string(localization_en, "en", "es")
+--
+localization_es = {
+    ["Action Button"] = "Botón de Acción",
+    ["Choices"] = "Opciones",
+    ["First Option"] = "Primera Opción",
+    ["Fourth Option"] = "Cuarta Opción",
+    ["Left Checkbox Option 1"] = "Opción de Casilla de Verificación Izquierda 1",
+    ["Left Checkbox Option 2"] = "Opción de Casilla de Verificación Izquierda 2",
+    ["Menu"] = "Menú",
+    ["Right Three-State Option"] = "Opción de Tres Estados a la Derecha",
+    ["Second Option"] = "Segunda Opción",
+    ["Short "] = "Corto ",
+    ["Test Autolayout"] = "Prueba de Autodiseño",
+    ["Third Option"] = "Tercera Opción",
+    ["This is long menu text "] = "Este es un texto de menú largo ",
+    ["This is long text choice "] = "Esta es una elección de texto largo ",
+    ["This is longer option text "] = "Este es un texto de opción más largo ",
+}
+
+--
+-- This table was auto-generated with localization_developer.translate_localized_table_string(localization_en, "en", "es")
+--
+localization_jp = {
+    ["Action Button"] = "アクションボタン",
+    ["Choices"] = "選択肢",
+    ["First Option"] = "最初のオプション",
+    ["Fourth Option"] = "第四のオプション",
+    ["Left Checkbox Option 1"] = "左チェックボックスオプション1",
+    ["Left Checkbox Option 2"] = "左チェックボックスオプション2",
+    ["Menu"] = "メニュー",
+    ["Right Three-State Option"] = "右三状態オプション",
+    ["Second Option"] = "第二のオプション",
+    ["Short "] = "短い ",
+    ["Test Autolayout"] = "テスト自動レイアウト",
+    ["Third Option"] = "第三のオプション",
+    ["This is long menu text "] = "これは長いメニューテキストです ",
+    ["This is long text choice "] = "これは長いテキスト選択です ",
+    ["This is longer option text "] = "これはより長いオプションテキストです ",
+}
+
+--
+-- This table was auto-generated with localization_developer.translate_localized_table_string(localization_en, "en", "de")
+--
+localization_de = {
+    ["Action Button"] = "Aktionsknopf",
+    ["Choices"] = "Auswahlmöglichkeiten",
+    ["First Option"] = "Erste Option",
+    ["Fourth Option"] = "Vierte Option",
+    ["Left Checkbox Option 1"] = "Linke Checkbox Option 1",
+    ["Left Checkbox Option 2"] = "Linke Checkbox Option 2",
+    ["Menu"] = "Menü",
+    ["Right Three-State Option"] = "Rechte Dreizustandsoption",
+    ["Second Option"] = "Zweite Option",
+    ["Short "] = "Kurz ",
+    ["Test Autolayout"] = "Test Autolayout",
+    ["Third Option"] = "Dritte Option",
+    ["This is long menu text "] = "Dies ist ein langer Menütext ",
+    ["This is long text choice "] = "Dies ist eine lange Textauswahl ",
+    ["This is longer option text "] = "Dies ist ein längerer Optionstext ",
+}
+
+localization_fr = {
+    ["Action Button"] = "Bouton d'action",
+    ["Choices"] = "Choix",
+    ["First Option"] = "Première Option",
+    ["Fourth Option"] = "Quatrième Option",
+    ["Left Checkbox Option 1"] = "Option de case à cocher gauche 1",
+    ["Left Checkbox Option 2"] = "Option de case à cocher gauche 2",
+    ["Menu"] = "Menu",
+    ["Right Three-State Option"] = "Option à trois états à droite",
+    ["Second Option"] = "Deuxième Option",
+    ["Short "] = "Court ",
+    ["Test Autolayout"] = "Test Autolayout",
+    ["Third Option"] = "Troisième Option",
+    ["This is long menu text "] = "Ceci est un long texte de menu ",
+    ["This is long text choice "] = "Ceci est un long choix de texte ",
+    ["This is longer option text "] = "Ceci est un texte d'option plus long ",
+}
+
+localization_zh = {
+    ["Action Button"] = "操作按钮",
+    ["Choices"] = "选择:",
+    ["First Option"] = "第一选项:",
+    ["Fourth Option"] = "第四选项:",
+    ["Left Checkbox Option 1"] = "左侧复选框选项1",
+    ["Left Checkbox Option 2"] = "左侧复选框选项2",
+    ["Menu"] = "菜单:",
+    ["Right Three-State Option"] = "右侧三态选项",
+    ["Second Option"] = "第二选项:",
+    ["Short "] = "短 ",
+    ["Test Autolayout"] = "测试自动布局",
+    ["Third Option"] = "第三选项:",
+    ["This is long menu text "] = "这是长菜单文本 ",
+    ["This is long text choice "] = "这是长文本选择 ",
+    ["This is longer option text "] = "这是更长的选项文本 ",
+}
+
+localization_ar = {
+    ["Action Button"] = "زر العمل",
+    ["Choices"] = "الخيارات",
+    ["First Option"] = "الخيار الأول",
+    ["Fourth Option"] = "الخيار الرابع",
+    ["Left Checkbox Option 1"] = "خيار المربع الأول على اليسار",
+    ["Left Checkbox Option 2"] = "خيار المربع الثاني على اليسار",
+    ["Menu"] = "القائمة",
+    ["Right Three-State Option"] = "خيار الحالة الثلاثية اليمين",
+    ["Second Option"] = "الخيار الثاني",
+    ["Short "] = "قصير ",
+    ["Test Autolayout"] = "اختبار التخطيط التلقائي",
+    ["Third Option"] = "الخيار الثالث",
+    ["This is long menu text "] = "هذا نص قائمة طويل ",
+    ["This is long text choice "] = "هذا خيار نص طويل ",
+    ["This is longer option text "] = "هذا نص خيار أطول ",
+}
+
+localization_fa = {
+    ["Action Button"] = "دکمه عملیات",
+    ["Choices"] = "گزینه ها",
+    ["First Option"] = "گزینه اول",
+    ["Fourth Option"] = "گزینه چهارم",
+    ["Left Checkbox Option 1"] = "گزینه چک باکس سمت چپ 1",
+    ["Left Checkbox Option 2"] = "گزینه چک باکس سمت چپ 2",
+    ["Menu"] = "منو",
+    ["Right Three-State Option"] = "گزینه سه حالته سمت راست",
+    ["Second Option"] = "گزینه دوم",
+    ["Short "] = "کوتاه ",
+    ["Test Autolayout"] = "تست خودکار طرح بندی",
+    ["Third Option"] = "گزینه سوم",
+    ["This is long menu text "] = "این متن منوی طولانی است ",
+    ["This is long text choice "] = "این یک انتخاب متن طولانی است ",
+    ["This is longer option text "] = "این متن گزینه طولانی تر است ",
+}
+
+localization.set_language("fa")
 
 function create_dialog()
     local dlg = mixin.FCXCustomLuaWindow()
-    dlg:SetTitle(finale.FCString("Test Autolayout"))
+    dlg:SetTitle(localization.localize("Test Autolayout"))
 
     local y = 0
     local line_no = 0
@@ -21,7 +184,7 @@ function create_dialog()
     dlg:CreateStatic(0, line_no * y_increment, "option1-label")
         :DoAutoResizeWidth(true)
         :SetWidth(0)
-        :SetText("First Option:")
+        :SetText(localization.localize("First Option"))
     dlg:CreateEdit(0, line_no * y_increment - utils.win_mac(2, 3), "option1")
         :SetInteger(1)
         :AssureNoHorizontalOverlap(dlg:GetControl("option1-label"), label_edit_separ)
@@ -30,13 +193,13 @@ function create_dialog()
     dlg:CreateCheckbox(0, line_no * y_increment, "left-checkbox1")
         :DoAutoResizeWidth(true)
         :SetWidth(0)
-        :SetText("Left Checkbox Option 1")
+        :SetText(localization.localize("Left Checkbox Option 1"))
     line_no = line_no + 1
 
     dlg:CreateStatic(0, line_no * y_increment, "option2-label")
         :DoAutoResizeWidth(true)
         :SetWidth(0)
-        :SetText("Second Option:")
+        :SetText(localization.localize("Second Option"))
     dlg:CreateEdit(0, line_no * y_increment - utils.win_mac(2, 3), "option2")
         :SetInteger(2)
         :AssureNoHorizontalOverlap(dlg:GetControl("option2-label"), label_edit_separ)
@@ -46,7 +209,7 @@ function create_dialog()
     dlg:CreateCheckbox(0, line_no * y_increment, "left-checkbox2")
         :DoAutoResizeWidth(true)
         :SetWidth(0)
-        :SetText("Left Checkbox Option 2")
+        :SetText(localization.localize("Left Checkbox Option 2"))
     line_no = line_no + 1
 
     -- center vertical line
@@ -61,7 +224,7 @@ function create_dialog()
     dlg:CreateStatic(0, line_no * y_increment, "option3-label")
         :DoAutoResizeWidth(true)
         :SetWidth(0)
-        :SetText("Third Option:")
+        :SetText(localization.localize("Third Option"))
         :AssureNoHorizontalOverlap(vertical_line, center_padding)
     dlg:CreateEdit(0, line_no * y_increment - utils.win_mac(2, 3), "option3")
         :SetInteger(3)
@@ -71,7 +234,7 @@ function create_dialog()
     dlg:CreateCheckbox(0, line_no * y_increment, "right-checkbox1")
         :DoAutoResizeWidth(true)
         :SetWidth(0)
-        :SetText("Right Three-State Option")
+        :SetText(localization.localize("Right Three-State Option"))
         :SetThreeStatesMode(true)
         :AssureNoHorizontalOverlap(vertical_line, center_padding)
     line_no = line_no + 1
@@ -79,7 +242,7 @@ function create_dialog()
     dlg:CreateStatic(0, line_no * y_increment, "option4-label")
         :DoAutoResizeWidth(true)
         :SetWidth(0)
-        :SetText("Fourth Option:")
+        :SetText(localization.localize("Fourth Option"))
         :AssureNoHorizontalOverlap(vertical_line, center_padding)
     dlg:CreateEdit(0, line_no * y_increment - utils.win_mac(2, 3), "option4")
         :SetInteger(4)
@@ -90,7 +253,7 @@ function create_dialog()
     dlg:CreateButton(0, line_no * y_increment)
         :DoAutoResizeWidth(true)
         :SetWidth(0)
-        :SetText("Action Button")
+        :SetText(localization.localize("Action Button"))
         :AssureNoHorizontalOverlap(vertical_line, center_padding)
         :HorizontallyAlignRightWith(dlg:GetControl("option4"))
     line_no = line_no + 1
@@ -105,16 +268,16 @@ function create_dialog()
     dlg:CreateStatic(0, line_no * y_increment, "popup_label")
         :DoAutoResizeWidth(true)
         :SetWidth(0)
-        :SetText("Menu:")
+        :SetText(localization.localize("Menu"))
     local ctrl_popup = dlg:CreatePopup(0, line_no * y_increment - utils.win_mac(2, 2), "popup")
         :DoAutoResizeWidth(true)
         :SetWidth(0)
         :AssureNoHorizontalOverlap(dlg:GetControl("popup_label"), label_edit_separ)
     for counter = 1, 3 do
         if counter == 3 then
-            ctrl_popup:AddString(finale.FCString("This is long menu text " .. counter))
+            ctrl_popup:AddString(finale.FCString(localization.localize("This is long menu text ") .. counter))
         else
-            ctrl_popup:AddString(finale.FCString("Short " .. counter))
+            ctrl_popup:AddString(finale.FCString(localization.localize("Short ") .. counter))
         end
     end
     ctrl_popup:SetSelectedItem(0)
@@ -123,7 +286,7 @@ function create_dialog()
     dlg:CreateStatic(0, line_no * y_increment, "cbobox_label")
         :DoAutoResizeWidth(true)
         :SetWidth(0)
-        :SetText("Choices:")
+        :SetText(localization.localize("Choices"))
     local ctrl_cbobox = dlg:CreateComboBox(0, line_no * y_increment - utils.win_mac(2, 3), "cbobox")
         :DoAutoResizeWidth(true)
         :SetWidth(40)
@@ -131,9 +294,9 @@ function create_dialog()
         :HorizontallyAlignLeftWith(ctrl_popup)
     for counter = 1, 3 do
         if counter == 3 then
-            ctrl_cbobox:AddString(finale.FCString("This is long text choice " .. counter))
+            ctrl_cbobox:AddString(finale.FCString(localization.localize("This is long text choice ") .. counter))
         else
-            ctrl_cbobox:AddString(finale.FCString("Short " .. counter))
+            ctrl_cbobox:AddString(finale.FCString(localization.localize("Short ") .. counter))
         end
     end
     ctrl_cbobox:SetSelectedItem(0)
@@ -148,9 +311,9 @@ function create_dialog()
             :AssureNoHorizontalOverlap(ctrl_popup, 10)
             :AssureNoHorizontalOverlap(ctrl_cbobox, 10)
         if counter == 2 then
-            rbtn:SetText(finale.FCString("This is longer option text " .. counter))
+            rbtn:SetText(finale.FCString(localization.localize("This is longer option text ") .. counter))
         else
-            rbtn:SetText(finale.FCString("Short " .. counter))
+            rbtn:SetText(finale.FCString(localization.localize("Short ") .. counter))
         end
         counter = counter + 1
     end
diff --git a/samples/auto_layout_localizing_script.lua b/samples/auto_layout_localizing_script.lua
new file mode 100644
index 00000000..2efe0c2c
--- /dev/null
+++ b/samples/auto_layout_localizing_script.lua
@@ -0,0 +1,31 @@
+function plugindef()
+    finaleplugin.RequireDocument = false
+    finaleplugin.MinJWLuaVersion = 0.71
+    finaleplugin.ExecuteHttpsCalls = true
+end
+
+--
+-- this table was copied from "auto_layout.lua" for the purpose of translating it
+--
+localization_en =
+{
+    ["Action Button"] = "Action Button",
+    ["Choices:"] = "Choices:",
+    ["First Option:"] = "First Option:",
+    ["Fourth Option:"] = "Fourth Option:",
+    ["Left Checkbox Option 1"] = "Left Checkbox Option 1",
+    ["Left Checkbox Option 2"] = "Left Checkbox Option 2",
+    ["Menu:"] = "Menu:",
+    ["Right Three-State Option"] = "Right Three-State Option",
+    ["Second Option:"] = "Second Option:",
+    ["Short "] = "Short ",
+    ["Test Autolayout"] = "Test Autolayout",
+    ["Third Option:"] = "Third Option:",
+    ["This is long menu text "] = "This is long menu text ",
+    ["This is long text choice "] = "This is long text choice ",
+    ["This is longer option text "] = "This is longer option text ",
+}
+
+local ldev = require('library.localization_developer')
+ldev.translate_localized_table_string(localization_en, "en", "ar")
+
diff --git a/src/library/client.lua b/src/library/client.lua
index 04fee5c3..ccc4cf8d 100644
--- a/src/library/client.lua
+++ b/src/library/client.lua
@@ -17,7 +17,7 @@ end
 
 local function requires_later_plugin_version(feature)
     if feature then
-        return "This script uses " .. to_human_string(feature) .. "which is only available in a later version of RGP Lua. Please update RGP Lua instead to use this script."
+        return "This script uses " .. to_human_string(feature) .. " which is only available in a later version of RGP Lua. Please update RGP Lua instead to use this script."
     end
     return "This script requires a later version of RGP Lua. Please update RGP Lua instead to use this script."
 end
@@ -108,6 +108,10 @@ local features = {
         test = finenv.RawFinaleVersion >= client.get_raw_finale_version(27, 1),
         error = requires_finale_version("27.1", "a SMUFL font"),
     },
+    luaosutils = {
+        test = finenv.EmbeddedLuaOSUtils,
+        error = requires_later_plugin_version("the embedded luaosutils library")
+    }
 }
 
 --[[
@@ -156,4 +160,32 @@ function client.assert_supports(feature)
     return true
 end
 
+
+--[[
+% encode_with_client_codepage
+
+If the client supports LuaOSUtils, the filepath is encoded from UTF-8 to the current client
+encoding. On macOS, this is always also UTF-8, so the situation where the string may be re-encoded
+is only on Windows. (Recent versions of Windows also allow UTF-8 as the client encoding, so it may
+not be re-encoded even on Windows.)
+
+If LuaOSUtils is not available, the string is returned unchanged.
+
+A primary use-case for this function is filepaths. Windows requires 8-bit filepaths to be encoded
+with the client codepage.
+
+@ input_string (string) the UTF-encoded string to re-encode
+: (string) the string re-encoded with the clieng codepage
+]]
+
+function client.encode_with_client_codepage(input_string)
+    if client.supports("luaosutils") then
+        local text = require("luaosutils").text
+        if text and text.get_default_codepage() ~= text.get_utf8_codepage() then
+            return text.convert_encoding(input_string, text.get_utf8_codepage(), text.get_default_codepage())
+        end
+    end
+    return input_string
+end
+
 return client
diff --git a/src/library/general_library.lua b/src/library/general_library.lua
index 73862cc6..9c556e7b 100644
--- a/src/library/general_library.lua
+++ b/src/library/general_library.lua
@@ -342,6 +342,7 @@ function library.get_smufl_font_list()
             -- Starting in 0.67, io.popen may fail due to being untrusted.
             local cmd = finenv.UI():IsOnWindows() and "dir " or "ls "
             local handle = io.popen(cmd .. options .. " \"" .. smufl_directory .. "\"")
+            if not handle then return "" end
             local retval = handle:read("*a")
             handle:close()
             return retval
@@ -573,14 +574,13 @@ function library.system_indent_set_to_prefs(system, page_format_prefs)
 end
 
 --[[
-% calc_script_name
+% calc_script_filepath
 
-Returns the running script name, with or without extension.
+Returns the full filepath of the running script.
 
-@ [include_extension] (boolean) Whether to include the file extension in the return value: `false` if omitted
-: (string) The name of the current running script.
+: (string) a string containing the filepath, encoded in UTF-8
 ]]
-function library.calc_script_name(include_extension)
+function library.calc_script_filepath()
     local fc_string = finale.FCString()
     if finenv.RunningLuaFilePath then
         -- Use finenv.RunningLuaFilePath() if available because it doesn't ever get overwritten when retaining state.
@@ -590,6 +590,20 @@ function library.calc_script_name(include_extension)
         -- SetRunningLuaFilePath is not reliable when retaining state, so later versions use finenv.RunningLuaFilePath.
         fc_string:SetRunningLuaFilePath()
     end
+    return fc_string.LuaString
+end
+
+--[[
+% calc_script_name
+
+Returns the running script name, with or without extension.
+
+@ [include_extension] (boolean) Whether to include the file extension in the return value: `false` if omitted
+: (string) The name of the current running script.
+]]
+function library.calc_script_name(include_extension)
+    local fc_string = finale.FCString()
+    fc_string.LuaString = library.calc_script_filepath()
     local filename_string = finale.FCString()
     fc_string:SplitToPathAndFile(nil, filename_string)
     local retval = filename_string.LuaString
diff --git a/src/library/localization.lua b/src/library/localization.lua
new file mode 100644
index 00000000..e7d3fb99
--- /dev/null
+++ b/src/library/localization.lua
@@ -0,0 +1,81 @@
+--[[
+$module Localization
+
+This library provides localization services to scripts. To use it, scripts must define each localization
+as a global table with the 2-letter language code appended.
+
+```
+localization_en = {
+    ["Hello"] = "Hello",
+    ["Goodbye"] = "Goodbye"
+}
+
+localization_es = {
+    ["Hello"] = "Hola",
+    ["Goodbye"] = "Adios"
+}
+
+localization_jp = {
+    ["Hello"] = "今日は",
+    ["Goodbye"] = "さようなら"
+}
+```
+
+The keys do not have to be in English, but they should be the same in all tables. You can embed the localizations
+in your script or include them with require. Example:
+
+```
+local language_code = "de" -- get this from `finenv.UI():GetUserLocaleName()`
+local localization_table_name = "localization_" language_code
+_G[localization_table_name] = require(localization_table_name)
+```
+
+In this case, `localization_de.lua` could be installed in the folder alongside the localized script. This is just
+an example. You can manage the dependencies however is best for your script. The easiest deployment will always be
+to avoid dependencies and embed the localizations in your script.
+
+The `library.localization_developer` library provides tools for automatically generating localization tables to
+copy into scripts. You can then edit them to suit your needs.
+]]
+
+local localization = {}
+
+local localization_language = (function()
+        if finenv.UI().GetUserLocaleName then
+            local fcstr = finale.FCString()
+            finenv.UI():GetUserLocaleName(fcstr)
+            return fcstr.LuaString:sub(1, 2)
+        end
+        return nil
+    end)()
+
+--[[
+% set_language
+
+Sets the localization language to a specified value. By default, the localization language is the 2-letter language
+code extracted from finenv.UI():GetUserLocaleName. If you are running a version of Finale Lua that does not have
+GetUserLocaleName, you must manually set the language from your script.
+
+This function can also be used to test different localizations without the need to switch user preferences in the OS.
+
+@ language_code (string) the two-letter lowercase language code of the language to use
+]]
+function localization.set_language(language_code)
+    localization_language = language_code
+end
+
+--[[
+% localize
+
+Localizes a string based on the localization language
+
+@ input_string (string) the string to be localized
+: (string) the localized version of the string or input_string if not found
+]]
+function localization.localize(input_string)
+    assert(type(localization_language) == "string", "no localization language is set")
+    local t = _G["localization_" .. localization_language]
+    return t and t[input_string] or input_string
+end
+
+return localization
diff --git a/src/library/localization_developer.lua b/src/library/localization_developer.lua
new file mode 100644
index 00000000..3c58ff4f
--- /dev/null
+++ b/src/library/localization_developer.lua
@@ -0,0 +1,140 @@
+--[[
+$module Localization for Developers
+
+This library provides a set of localization services for developers of scripts to make localization
+as simple as possible. It uses calls to OpenAI to automatically translate words and phrases.
+]]
+
+local localization_developer = {}
+
+local client = require("library.client")
+local library = require("library.general_library")
+local openai = require("library.openai")
+
+--[[
+% create_localized_base_table
+
+Creates and returns a table of localizable strings by searching the top-level script for
+quoted strings. While this may be useful at user-runtime, the primary use case it targets
+is as a developer tool to aid in the creation of a table to be embedded in the script.
+
+The returned table is in this form:
+
+```
+{
+    ["<found string>"] = "found-string",
+    ... -- for every string found in the script
+}
+
+Only the top-level script is searched. This is the script at the path specified by finenv.Running
+
+: (table) a table containing the found strings
+]]
+function localization_developer.create_localized_base_table()
+    local retval = {}
+    local file_path = library.calc_script_filepath()
+    file_path = client.encode_with_client_codepage(file_path)
+    local file = io.open(file_path, "r")
+    if file then
+        local file_content = file:read("all")
+        local function extract_strings(file_content)
+            local i = 1
+            local length = #file_content
+            return function()
+                while i <= length do
+                    local char = string.sub(file_content, i, i)
+                    if char == "'" or char == '"' then
+                        local quote = char
+                        local str = quote
+                        i = i + 1
+                        while i <= length do
+                            char = string.sub(file_content, i, i)
+                            local escaped = false
+                            if char == '\\' then
+                                i = i + 1
+                                char = string.sub(file_content, i, i)
+                                if char == "n" then char = "\n" end
+                                if char == "r" then char = "\r" end
+                                if char == "t" then char = "\t" end
+                                -- may need to add more escape codes here
+                                escaped = true
+                            end
+                            str = str .. char
+                            -- Check for the end of the quoted string
+                            if not escaped and char == quote then
+                                break
+                            end
+                            i = i + 1
+                        end
+                        i = i + 1
+                        return str:sub(2, -2)
+                    end
+                    i = i + 1
+                end
+                -- End of file, return nil to terminate the loop
+                return nil
+            end
+        end
+        for found_string in extract_strings(file_content) do
+            retval[found_string] = found_string
+        end
+        file:close()
+    end
+    return retval
+end
+
+local function make_flat_table_string(lang, t)
+    local concat = {}
+    table.insert(concat, "localization_" .. lang .. " = {\n")
+    for k, v in pairsbykeys(t) do
+        table.insert(concat, "    [\"" .. tostring(k) .. "\"] = \"" .. tostring(v) .. "\",\n")
+    end
+    table.insert(concat, "}\n")
+    return table.concat(concat)
+end
+
+--[[
+% create_localized_base_table_string
+
+Creates and returns a string representing a lua table of localizable strings by searching the top-level script for
+quoted strings. It then copies this string to the clipboard. The primary use case is to be
+a developer tool to aid in the creation of a table to be embedded in the script.
+
+The base table is the table that defines the keys for all other languages. For each item in the base table, the
+key is always equal to the value. The base table can be in any language.
+
+@ lang (string) the two-letter language code of the strings in the base table. This is used only to name the table.
+: (string) A string containing a Lua-formatted table of all quoted strings in the script
+]]
+function localization_developer.create_localized_base_table_string(lang)
+    local t = localization_developer.create_localized_base_table()
+    finenv.UI():TextToClipboard(make_flat_table_string(lang, t))
+    finenv.UI():AlertInfo("localization_base table copied to clipboard", "")
+end
+
+function localization_developer.translate_localized_table_string(source_table, source_lang, target_lang)
+    local table_string = make_flat_table_string(source_lang, source_table)
+    local prompt = [[
+        I am working on localizing text for a program that prints and plays music. There may be musical
+        terminology among the words and phrases that I would like you to translate, as follows.\n
+    ]] .. "Here is a lua table of keys and values:\n\n```\n" .. table_string .. "\n```\n" ..
+                        [[
+                    Provide a string that is Lua source code of a table definition of a table that has the same keys
+                    but with the values translated to languages specified by the code
+                ]] .. target_lang .. ". The table name should be `localization_`" .. target_lang .. "`.\n" ..
+                [[
+                    Return only the Lua code without any commentary. There may or may not be musical terms
+                    in the provided text. This information is provided for context if needed.
+                ]]
+
+    local success, result = openai.create_completion("gpt-4", prompt, 0.2, 30)
+    if success then
+        local retval = string.gsub(result.choices[1].message.content, "```", "")
+        finenv.UI():TextToClipboard(retval)
+        finenv.UI():AlertInfo("localization_" .. target_lang .. " table copied to clipboard", "")
+    else
+        finenv.UI():AlertError(result, "OpenAI Error")
+    end
+end
+
+return localization_developer
diff --git a/src/library/tie.lua b/src/library/tie.lua
index 47079ed3..57a684ae 100644
--- a/src/library/tie.lua
+++ b/src/library/tie.lua
@@ -533,7 +533,7 @@ function tie.calc_placement(note, tie_mod, for_pageview, direction, tie_prefs)
         if end_note then
             local next_stemdir = end_note.Entry:CalcStemUp() and 1 or -1
             end_placement = calc_placement_for_endpoint(end_note, tie_mod, tie_prefs, direction, next_stemdir, true)
-        else
+        elseif start_note then
             -- more reverse-engineered logic. Here is the observed Finale behavior:
             -- 1. Ties to rests and nothing have StemOuter placement at their endpoint.
             -- 2. Ties to an adjacent empty bar have inner placement on both ends. (weird but true)
@@ -707,7 +707,7 @@ local calc_tie_length = function(note, tie_mod, for_pageview, direction, tie_pre
             horz_end = next_cell_metrics.MusicStartPos * staff_scaling
         end
         horz_end = horz_end / horz_stretch
-    else
+    elseif start_note then
         local entry_metrics = tie_mod:IsStartTie() and entry_metrics_end or entry_metrics_start
         local note_index = start_note.NoteIndex
         if end_note then

From 25af53c7178dbb0d04410f83592a8b061b8e3abe Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Thu, 18 Jan 2024 21:35:32 -0600
Subject: [PATCH 14/61] refactor localization to add the localization tables to
 the library itself.

---
 docs/rgp-lua.md                           | 34 ++++++++++-
 samples/auto_layout.lua                   | 47 ++++++++-------
 samples/auto_layout_localizing_script.lua | 21 +++----
 src/library/localization.lua              | 73 +++++++++++++++--------
 src/library/localization_developer.lua    |  6 +-
 5 files changed, 119 insertions(+), 62 deletions(-)

diff --git a/docs/rgp-lua.md b/docs/rgp-lua.md
index 129c10ae..2c99335f 100644
--- a/docs/rgp-lua.md
+++ b/docs/rgp-lua.md
@@ -17,7 +17,7 @@ print ("Hello, world!")
 If you want a “Hello, world!” example that shows up as a menu option in Finale's Plug-in menu, here is a slightly more complex version:
 
 ```lua
-function plugindef()
+function plugindef(locale)
     return "Hello World", "Hello World", 'Displays a message box saying, "Hello, world!"'
 end
 
@@ -285,7 +285,6 @@ The `plugindef()` function is an optional function that **only** should do a _ma
 * Return the _plug-in name_, _undo string_ and _brief description_ to be used in the _Finale_ plug-in menu and for automatic undo blocks.
 * Define the `finaleplugin` namespace environment to further describe the plug-in (see below).
 
-
 A simple `plugindef()` implementation might look like this:
 
 ```lua
@@ -296,7 +295,36 @@ function plugindef()
 end
 ```
 
-`plugindef()` is considered to be a reserved name in the global namespace. If the script has a function named `plugindef()`, the Lua plugin may call it at any time (not only during script execution) to gather information about the plug-in. The `plugindef()` function can **NOT** have dependencies outside the function itself.
+Starting with version 0.71, _RGP Lua_ passes the user's locale code as an argument to the `plugindef` function. You can use this to localize any strings returned by the function or assigned to variables. The user's locale code is a 2-character lowercase language code followed by "_" or "-" and then a 2-digit uppercase region code. This is the same value that is returned by `finenv.UI():GetUserLocaleName()`. (See the note below detailing why the `plugindef` function cannot call `GetUserLocaleName` directly.)
+
+A localized version of the same function might look like this:
+
+```lua
+function plugindef(locale)
+    local localization = {}
+    localization.en = {
+        ["Hide Rests"] = "Hide Rests",
+        ["Hides all rests in the selected region."] = "Hides all rests in the selected region."
+    }
+    localization.es = {
+        ["Hide Rests"] = "Ocultar Silencios",
+        ["Hides all rests in the selected region."] = "Oculta todos los silencios en la región seleccionada."
+    }
+    localization.jp = {
+        ["Hide Rests"] = "休符を隠す",
+        ["Hides all rests in the selected region."] = "選択した領域内のすべての休符を隠します。",
+    }
+    -- add more localizations as desired
+    local t = locale and localization[locale:sub(1,2)] or localization.en
+    finaleplugin.RequireSelection = true
+    finaleplugin.CategoryTags = "Rest, Region"
+    return t["Hide Rests"], t["Hide Rests"], t["Hides all rests in the selected region."]
+end
+```
+
+Note that the `plugindef()` function must be *entirely* self-contained. It may not have access to any of the global namespaces that the rest of the script uses, such as `finenv` or `finale`. It *does* have access to all the standard Lua libraries. If the script has a function named `plugindef()`, the Lua plugin may call it at any time (not only during script execution) to gather information about the plug-in.
+
+`plugindef` is a reserved name in the global namespace.
 
 All aspects of the `plugindef()` are optional, but for a plug-in script that is going to be used repeatedly, the minimum should be to return a plug-in name, undo string, and short description.
 
diff --git a/samples/auto_layout.lua b/samples/auto_layout.lua
index 7f067d0c..653b775a 100644
--- a/samples/auto_layout.lua
+++ b/samples/auto_layout.lua
@@ -12,7 +12,7 @@ local localization = require('library.localization')
 -- This table was auto-generated with localization_developer.create_localized_base_table_string(en)
 -- Then it was edited to include only the strings that need to be localized.
 --
-localization_en =
+localization.en = -- this is en_GB due to spelling of "Localisation"
 {
     ["Action Button"] = "Action Button",
     ["Choices"] = "Choices",
@@ -24,19 +24,24 @@ localization_en =
     ["Right Three-State Option"] = "Right Three-State Option",
     ["Second Option"] = "Second Option",
     ["Short "] = "Short ",
-    ["Test Autolayout"] = "Test Autolayout",
+    ["Test Autolayout With Localisation"] = "Test Autolayout With Localisation",
     ["Third Option"] = "Third Option",
     ["This is long menu text "] = "This is long menu text ",
     ["This is long text choice "] = "This is long text choice ",
     ["This is longer option text "] = "This is longer option text ",
 }
 
+localization.en_US =
+{
+    ["Test Autolayout With Localisation"] = "Test Autolayout With Localization",
+}
+
 --
 -- The rest of the localization tables were created one-at-a-time with the auto_layout_localizing_script.lua
 --
--- This table was auto-generated with localization_developer.translate_localized_table_string(localization_en, "en", "es")
+-- This table was auto-generated with localization_developer.translate_localized_table_string(localization.en, "en", "es")
 --
-localization_es = {
+localization.es = {
     ["Action Button"] = "Botón de Acción",
     ["Choices"] = "Opciones",
     ["First Option"] = "Primera Opción",
@@ -47,7 +52,7 @@ localization_es = {
     ["Right Three-State Option"] = "Opción de Tres Estados a la Derecha",
     ["Second Option"] = "Segunda Opción",
     ["Short "] = "Corto ",
-    ["Test Autolayout"] = "Prueba de Autodiseño",
+    ["Test Autolayout With Localisation"] = "Prueba de Autodiseño con Localización",
     ["Third Option"] = "Tercera Opción",
     ["This is long menu text "] = "Este es un texto de menú largo ",
     ["This is long text choice "] = "Esta es una elección de texto largo ",
@@ -55,9 +60,9 @@ localization_es = {
 }
 
 --
--- This table was auto-generated with localization_developer.translate_localized_table_string(localization_en, "en", "es")
+-- This table was auto-generated with localization_developer.translate_localized_table_string(localization.en, "en", "es")
 --
-localization_jp = {
+localization.jp = {
     ["Action Button"] = "アクションボタン",
     ["Choices"] = "選択肢",
     ["First Option"] = "最初のオプション",
@@ -68,7 +73,7 @@ localization_jp = {
     ["Right Three-State Option"] = "右三状態オプション",
     ["Second Option"] = "第二のオプション",
     ["Short "] = "短い ",
-    ["Test Autolayout"] = "テスト自動レイアウト",
+    ["Test Autolayout With Localisation"] = "ローカリゼーションでのオートレイアウトのテスト",
     ["Third Option"] = "第三のオプション",
     ["This is long menu text "] = "これは長いメニューテキストです ",
     ["This is long text choice "] = "これは長いテキスト選択です ",
@@ -76,9 +81,9 @@ localization_jp = {
 }
 
 --
--- This table was auto-generated with localization_developer.translate_localized_table_string(localization_en, "en", "de")
+-- This table was auto-generated with localization_developer.translate_localized_table_string(localization.en, "en", "de")
 --
-localization_de = {
+localization.de = {
     ["Action Button"] = "Aktionsknopf",
     ["Choices"] = "Auswahlmöglichkeiten",
     ["First Option"] = "Erste Option",
@@ -89,14 +94,14 @@ localization_de = {
     ["Right Three-State Option"] = "Rechte Dreizustandsoption",
     ["Second Option"] = "Zweite Option",
     ["Short "] = "Kurz ",
-    ["Test Autolayout"] = "Test Autolayout",
+    ["Test Autolayout With Localisation"] = "Test von Autolayout mit Lokalisierung",
     ["Third Option"] = "Dritte Option",
     ["This is long menu text "] = "Dies ist ein langer Menütext ",
     ["This is long text choice "] = "Dies ist eine lange Textauswahl ",
     ["This is longer option text "] = "Dies ist ein längerer Optionstext ",
 }
 
-localization_fr = {
+localization.fr = {
     ["Action Button"] = "Bouton d'action",
     ["Choices"] = "Choix",
     ["First Option"] = "Première Option",
@@ -107,14 +112,14 @@ localization_fr = {
     ["Right Three-State Option"] = "Option à trois états à droite",
     ["Second Option"] = "Deuxième Option",
     ["Short "] = "Court ",
-    ["Test Autolayout"] = "Test Autolayout",
+    ["Test Autolayout With Localisation"] = "Test de AutoLayout avec Localisation",
     ["Third Option"] = "Troisième Option",
     ["This is long menu text "] = "Ceci est un long texte de menu ",
     ["This is long text choice "] = "Ceci est un long choix de texte ",
     ["This is longer option text "] = "Ceci est un texte d'option plus long ",
 }
 
-localization_zh = {
+localization.zh = {
     ["Action Button"] = "操作按钮",
     ["Choices"] = "选择:",
     ["First Option"] = "第一选项:",
@@ -125,14 +130,14 @@ localization_zh = {
     ["Right Three-State Option"] = "右侧三态选项",
     ["Second Option"] = "第二选项:",
     ["Short "] = "短 ",
-    ["Test Autolayout"] = "测试自动布局",
+    ["Test Autolayout With Localisation"] = "自动布局与本地化测试",
     ["Third Option"] = "第三选项:",
     ["This is long menu text "] = "这是长菜单文本 ",
     ["This is long text choice "] = "这是长文本选择 ",
     ["This is longer option text "] = "这是更长的选项文本 ",
 }
 
-localization_ar = {
+localization.ar = {
     ["Action Button"] = "زر العمل",
     ["Choices"] = "الخيارات",
     ["First Option"] = "الخيار الأول",
@@ -143,14 +148,14 @@ localization_ar = {
     ["Right Three-State Option"] = "خيار الحالة الثلاثية اليمين",
     ["Second Option"] = "الخيار الثاني",
     ["Short "] = "قصير ",
-    ["Test Autolayout"] = "اختبار التخطيط التلقائي",
+    ["Test Autolayout With Localisation"] = "اختبار التخطيط التلقائي مع التعريب",
     ["Third Option"] = "الخيار الثالث",
     ["This is long menu text "] = "هذا نص قائمة طويل ",
     ["This is long text choice "] = "هذا خيار نص طويل ",
     ["This is longer option text "] = "هذا نص خيار أطول ",
 }
 
-localization_fa = {
+localization.fa = {
     ["Action Button"] = "دکمه عملیات",
     ["Choices"] = "گزینه ها",
     ["First Option"] = "گزینه اول",
@@ -161,18 +166,18 @@ localization_fa = {
     ["Right Three-State Option"] = "گزینه سه حالته سمت راست",
     ["Second Option"] = "گزینه دوم",
     ["Short "] = "کوتاه ",
-    ["Test Autolayout"] = "تست خودکار طرح بندی",
+    ["Test Autolayout With Localisation"] = "تست آتولایوت با بومی سازی",
     ["Third Option"] = "گزینه سوم",
     ["This is long menu text "] = "این متن منوی طولانی است ",
     ["This is long text choice "] = "این یک انتخاب متن طولانی است ",
     ["This is longer option text "] = "این متن گزینه طولانی تر است ",
 }
 
-localization.set_language("fa")
+--localization.set_locale("fa")
 
 function create_dialog()
     local dlg = mixin.FCXCustomLuaWindow()
-    dlg:SetTitle(localization.localize("Test Autolayout"))
+    dlg:SetTitle(localization.localize("Test Autolayout With Localisation"))
 
     local y = 0
     local line_no = 0
diff --git a/samples/auto_layout_localizing_script.lua b/samples/auto_layout_localizing_script.lua
index 2efe0c2c..5918159e 100644
--- a/samples/auto_layout_localizing_script.lua
+++ b/samples/auto_layout_localizing_script.lua
@@ -4,28 +4,29 @@ function plugindef()
     finaleplugin.ExecuteHttpsCalls = true
 end
 
+local localization = require("library.localization")
+
 --
 -- this table was copied from "auto_layout.lua" for the purpose of translating it
 --
-localization_en =
+localization.en =
 {
     ["Action Button"] = "Action Button",
-    ["Choices:"] = "Choices:",
-    ["First Option:"] = "First Option:",
-    ["Fourth Option:"] = "Fourth Option:",
+    ["Choices"] = "Choices",
+    ["First Option"] = "First Option",
+    ["Fourth Option"] = "Fourth Option",
     ["Left Checkbox Option 1"] = "Left Checkbox Option 1",
     ["Left Checkbox Option 2"] = "Left Checkbox Option 2",
-    ["Menu:"] = "Menu:",
+    ["Menu"] = "Menu",
     ["Right Three-State Option"] = "Right Three-State Option",
-    ["Second Option:"] = "Second Option:",
+    ["Second Option"] = "Second Option",
     ["Short "] = "Short ",
-    ["Test Autolayout"] = "Test Autolayout",
-    ["Third Option:"] = "Third Option:",
+    ["Test Autolayout With Localisation"] = "Test Autolayout With Localisation",
+    ["Third Option"] = "Third Option",
     ["This is long menu text "] = "This is long menu text ",
     ["This is long text choice "] = "This is long text choice ",
     ["This is longer option text "] = "This is longer option text ",
 }
 
 local ldev = require('library.localization_developer')
-ldev.translate_localized_table_string(localization_en, "en", "ar")
-
+ldev.translate_localized_table_string(localization.en, "en", "en_GB")
diff --git a/src/library/localization.lua b/src/library/localization.lua
index e7d3fb99..b65b87c5 100644
--- a/src/library/localization.lua
+++ b/src/library/localization.lua
@@ -2,37 +2,51 @@
 $module Localization
 
 This library provides localization services to scripts. To use it, scripts must define each localization
-as a global table with the 2-letter language code appended.
+as a table appended to this library table. If you provide region-specific localizations, you should also
+provide a generic localization for the 2-character language code as a fallback.
 
 ```
-localization_en = {
+local localization = require("library.localization")
+--
+-- append localizations to the library table:
+--
+localization.en = {
     ["Hello"] = "Hello",
-    ["Goodbye"] = "Goodbye"
+    ["Goodbye"] = "Goodbye",
+    ["Computer"] = "Computer"
 }
 
-localization_es = {
+localization.es = {
     ["Hello"] = "Hola",
-    ["Goodbye"] = "Adios"
+    ["Goodbye"] = "Adiós",
+    ["Computer"] = "Ordenador"
 }
 
-localization_jp = {
+-- specific localization for Mexico
+-- it is only necessary to specify items that are different from the fallback language table.
+localization.es_MX = {
+    ["Computer"] = "Computadora"
+}
+
+localization.jp = {
     ["Hello"] = "今日は",
-    ["Goodbye"] = "さようなら"
+    ["Goodbye"] = "さようなら",
+    ["Computer"] =  "コンピュータ" 
 }
 ```
 
 The keys do not have to be in English, but they should be the same in all tables. You can embed the localizations
-in your script or include them with require. Example:
+in your script or include them with `require`. Example:
 
 ```
-local language_code = "de" -- get this from `finenv.UI():GetUserLocaleName()`
-local localization_table_name = "localization_" language_code
-_G[localization_table_name] = require(localization_table_name)
+local region_code = "de_CH" -- get this from `finenv.UI():GetUserLocaleName(): you could also use just the language code "de"
+local localization_table_name = "localization_" region_code
+localization[region_code] = require(localization_table_name)
 ```
 
-In this case, `localization_de.lua` could be installed in the folder alongside the localized script. This is just
-an example. You can manage the dependencies however is best for your script. The easiest deployment will always be
-to avoid dependencies and embed the localizations in your script.
+In this case, `localization_de_CH.lua` could be installed in the folder alongside the localized script. This is just
+one possible approach. You can manage the dependencies in the manner that is best for your script. The easiest
+deployment will always be to avoid dependencies and embed the localizations in your script.
 
 The `library.localization_developer` library provides tools for automatically generating localization tables to
 copy into scripts. You can then edit them to suit your needs.
@@ -40,28 +54,27 @@ copy into scripts. You can then edit them to suit your needs.
 
 local localization = {}
 
-local localization_language = (function()
+local locale = (function()
         if finenv.UI().GetUserLocaleName then
             local fcstr = finale.FCString()
             finenv.UI():GetUserLocaleName(fcstr)
-            return fcstr.LuaString:sub(1, 2)
+            return fcstr.LuaString:gsub("-", "_")
         end
         return nil
     end)()
 
 --[[
-% set_language
+% set_locale
 
-Sets the localization language to a specified value. By default, the localization language is the 2-letter language
-code extracted from finenv.UI():GetUserLocaleName. If you are running a version of Finale Lua that does not have
-GetUserLocaleName, you must manually set the language from your script.
+Sets the locale to a specified value. By default, the locale language is the same value as finenv.UI():GetUserLocaleName.
+If you are running a version of Finale Lua that does not have GetUserLocaleName, you must manually set the locale from your script.
 
 This function can also be used to test different localizations without the need to switch user preferences in the OS.
 
-@ language_code (string) the two-letter lowercase language code of the language to use
+@ input_locale (string) the 2-letter lowercase language code or 5-character regional locale code
 ]]
-function localization.set_language(language_code)
-    localization_language = language_code
+function localization.set_locale(input_locale)
+    locale = input_locale:gsub("-", "_")
 end
 
 --[[
@@ -73,8 +86,18 @@ Localizes a string based on the localization language
 : (string) the localized version of the string or input_string if not found
 ]]
 function localization.localize(input_string)
-    assert(type(localization_language) == "string", "no localization language is set")
-    local t = _G["localization_" .. localization_language]
+    assert(type(locale) == "string", "no localization language is set")
+    assert(type(input_string) == "string", "expected string, got " .. type(input_string))
+
+    local t = localization[locale]
+    if t and t[input_string] then
+        return t[input_string]
+    end
+
+    if #locale > 2 then
+        t = localization[locale:sub(1, 2)]
+    end
+    
     return t and t[input_string] or input_string
 end
 
diff --git a/src/library/localization_developer.lua b/src/library/localization_developer.lua
index 3c58ff4f..c3c7aa8a 100644
--- a/src/library/localization_developer.lua
+++ b/src/library/localization_developer.lua
@@ -85,7 +85,7 @@ end
 
 local function make_flat_table_string(lang, t)
     local concat = {}
-    table.insert(concat, "localization_" .. lang .. " = {\n")
+    table.insert(concat, "localization." .. lang .. " = {\n")
     for k, v in pairsbykeys(t) do
         table.insert(concat, "    [\"" .. tostring(k) .. "\"] = \"" .. tostring(v) .. "\",\n")
     end
@@ -120,8 +120,8 @@ function localization_developer.translate_localized_table_string(source_table, s
     ]] .. "Here is a lua table of keys and values:\n\n```\n" .. table_string .. "\n```\n" ..
                         [[
                     Provide a string that is Lua source code of a table definition of a table that has the same keys
-                    but with the values translated to languages specified by the code
-                ]] .. target_lang .. ". The table name should be `localization_`" .. target_lang .. "`.\n" ..
+                    but with the values translated to locale specified by the code
+                ]] .. target_lang .. ". The table name should be `localization." .. target_lang .. "`.\n" ..
                 [[
                     Return only the Lua code without any commentary. There may or may not be musical terms
                     in the provided text. This information is provided for context if needed.

From c77f535b1e45024e61a373dc3ab580f2892c5596 Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Fri, 19 Jan 2024 09:48:01 -0600
Subject: [PATCH 15/61] localize menu options of Transpose By Steps

---
 src/transpose_by_step.lua | 26 ++++++++++++++++++++------
 1 file changed, 20 insertions(+), 6 deletions(-)

diff --git a/src/transpose_by_step.lua b/src/transpose_by_step.lua
index 7e3a350b..691627a4 100644
--- a/src/transpose_by_step.lua
+++ b/src/transpose_by_step.lua
@@ -1,4 +1,4 @@
-function plugindef()
+function plugindef(locale)
     finaleplugin.RequireSelection = false
     finaleplugin.HandlesUndo = true -- not recognized by JW Lua or RGP Lua v0.55
     finaleplugin.Author = "Robert Patterson"
@@ -14,23 +14,37 @@ function plugindef()
         Normally the script opens a modeless window. However, if you invoke the plugin with a shift, option, or
         alt key pressed, it skips opening a window and uses the last settings you entered into the window.
         (This works with RGP Lua version 0.60 and higher.)
-        
+
         If you are using custom key signatures with JW Lua or an early version of RGP Lua, you must create
         a custom_key_sig.config.txt file in a folder called `script_settings` within the same folder as the script.
         It should contains the following two lines that define the custom key signature you are using. Unfortunately,
         the JW Lua and early versions of RGP Lua do not allow scripts to read this information from the Finale document.
-        
+
         (This example is for 31-EDO.)
-        
+
         ```
         number_of_steps = 31
         diatonic_steps = {0, 5, 10, 13, 18, 23, 28}
         ```
-        
+
         Later versions of RGP Lua (0.58 or higher) ignore this configuration file (if it exists) and read the correct
         information from the Finale document.
     ]]
-    return "Transpose By Steps...", "Transpose By Steps", "Transpose by the number of steps given, simplifying spelling as needed."
+    local loc = {}
+    loc.en = {
+        menu = "Transpose By Steps",
+        desc = "Transpose by the number of steps given, simplifying spelling as needed."
+    }
+    loc.es = {
+        menu = "Transponer Por Pasos",
+        desc = "Transponer por el número de pasos dado, simplificando la ortografía según sea necesario.",
+    }
+    loc.de = {
+        menu = "Transponieren nach Schritten",
+        desc = "Transponieren Sie nach der angegebenen Anzahl von Schritten und vereinfachen Sie die Schreibweise nach Bedarf.",
+    }
+    local t = locale and loc[locale:sub(1,2)] or loc.en
+    return t.menu .. "...", t.menu, t.desc
 end
 
 -- luacheck: ignore 11./global_dialog

From 5051a36964f54ad050731bbf2bf486210540a881 Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Fri, 19 Jan 2024 13:31:44 -0600
Subject: [PATCH 16/61] docs, tranposition localization

---
 docs/rgp-lua.md           | 18 +++++++++---------
 src/transpose_by_step.lua |  6 +++---
 2 files changed, 12 insertions(+), 12 deletions(-)

diff --git a/docs/rgp-lua.md b/docs/rgp-lua.md
index 2c99335f..31d2781b 100644
--- a/docs/rgp-lua.md
+++ b/docs/rgp-lua.md
@@ -301,24 +301,24 @@ A localized version of the same function might look like this:
 
 ```lua
 function plugindef(locale)
+    finaleplugin.RequireSelection = true
+    finaleplugin.CategoryTags = "Rest, Region"
     local localization = {}
     localization.en = {
-        ["Hide Rests"] = "Hide Rests",
-        ["Hides all rests in the selected region."] = "Hides all rests in the selected region."
+        menu = "Hide Rests",
+        desc = "Hides all rests in the selected region."
     }
     localization.es = {
-        ["Hide Rests"] = "Ocultar Silencios",
-        ["Hides all rests in the selected region."] = "Oculta todos los silencios en la región seleccionada."
+        menu = "Ocultar Silencios",
+        desc = "Oculta todos los silencios en la región seleccionada."
     }
     localization.jp = {
-        ["Hide Rests"] = "休符を隠す",
-        ["Hides all rests in the selected region."] = "選択した領域内のすべての休符を隠します。",
+        menu = "休符を隠す",
+        desc = "選択した領域内のすべての休符を隠します。",
     }
     -- add more localizations as desired
     local t = locale and localization[locale:sub(1,2)] or localization.en
-    finaleplugin.RequireSelection = true
-    finaleplugin.CategoryTags = "Rest, Region"
-    return t["Hide Rests"], t["Hide Rests"], t["Hides all rests in the selected region."]
+    return t.menu, t.menu, t.desc
 end
 ```
 
diff --git a/src/transpose_by_step.lua b/src/transpose_by_step.lua
index 691627a4..288bfca2 100644
--- a/src/transpose_by_step.lua
+++ b/src/transpose_by_step.lua
@@ -33,15 +33,15 @@ function plugindef(locale)
     local loc = {}
     loc.en = {
         menu = "Transpose By Steps",
-        desc = "Transpose by the number of steps given, simplifying spelling as needed."
+        desc = "Transpose by the number of steps given, simplifying the note spelling as needed."
     }
     loc.es = {
         menu = "Transponer Por Pasos",
-        desc = "Transponer por el número de pasos dado, simplificando la ortografía según sea necesario.",
+        desc = "Transponer por el número de pasos dado, simplificando la enarmonización según sea necesario.",
     }
     loc.de = {
         menu = "Transponieren nach Schritten",
-        desc = "Transponieren Sie nach der angegebenen Anzahl von Schritten und vereinfachen Sie die Schreibweise nach Bedarf.",
+        desc = "Transponieren Sie nach der angegebenen Anzahl von Schritten und vereinfachen Sie die Notation nach Bedarf.",
     }
     local t = locale and loc[locale:sub(1,2)] or loc.en
     return t.menu .. "...", t.menu, t.desc

From ae478e35dce3055cc407a92263fc6944ec104355 Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Fri, 19 Jan 2024 15:22:25 -0600
Subject: [PATCH 17/61] localization of transpose

---
 src/library/localization.lua           | 12 +++++---
 src/library/localization_developer.lua |  4 +--
 src/transpose_by_step.lua              | 42 ++++++++++++++++++++++----
 3 files changed, 45 insertions(+), 13 deletions(-)

diff --git a/src/library/localization.lua b/src/library/localization.lua
index b65b87c5..0b6338d5 100644
--- a/src/library/localization.lua
+++ b/src/library/localization.lua
@@ -10,13 +10,13 @@ local localization = require("library.localization")
 --
 -- append localizations to the library table:
 --
-localization.en = {
+localization.en = localization.en or {
     ["Hello"] = "Hello",
     ["Goodbye"] = "Goodbye",
     ["Computer"] = "Computer"
 }
 
-localization.es = {
+localization.es = localization.es or {
     ["Hello"] = "Hola",
     ["Goodbye"] = "Adiós",
     ["Computer"] = "Ordenador"
@@ -24,11 +24,11 @@ localization.es = {
 
 -- specific localization for Mexico
 -- it is only necessary to specify items that are different from the fallback language table.
-localization.es_MX = {
+localization.es_MX = localization.es_MX or {
     ["Computer"] = "Computadora"
 }
 
-localization.jp = {
+localization.jp = localization.jp or {
     ["Hello"] = "今日は",
     ["Goodbye"] = "さようなら",
     ["Computer"] =  "コンピュータ" 
@@ -86,9 +86,11 @@ Localizes a string based on the localization language
 : (string) the localized version of the string or input_string if not found
 ]]
 function localization.localize(input_string)
-    assert(type(locale) == "string", "no localization language is set")
     assert(type(input_string) == "string", "expected string, got " .. type(input_string))
 
+    if not locale then return input_string end
+    assert(type(locale) == "string", "invalid locale setting " .. tostring(locale))
+    
     local t = localization[locale]
     if t and t[input_string] then
         return t[input_string]
diff --git a/src/library/localization_developer.lua b/src/library/localization_developer.lua
index c3c7aa8a..dfae8ccd 100644
--- a/src/library/localization_developer.lua
+++ b/src/library/localization_developer.lua
@@ -85,7 +85,7 @@ end
 
 local function make_flat_table_string(lang, t)
     local concat = {}
-    table.insert(concat, "localization." .. lang .. " = {\n")
+    table.insert(concat, "localization." .. lang .. " = "localization." .. lang .. " or {\n")
     for k, v in pairsbykeys(t) do
         table.insert(concat, "    [\"" .. tostring(k) .. "\"] = \"" .. tostring(v) .. "\",\n")
     end
@@ -131,7 +131,7 @@ function localization_developer.translate_localized_table_string(source_table, s
     if success then
         local retval = string.gsub(result.choices[1].message.content, "```", "")
         finenv.UI():TextToClipboard(retval)
-        finenv.UI():AlertInfo("localization_" .. target_lang .. " table copied to clipboard", "")
+        finenv.UI():AlertInfo("localization." .. target_lang .. " table copied to clipboard", "")
     else
         finenv.UI():AlertError(result, "OpenAI Error")
     end
diff --git a/src/transpose_by_step.lua b/src/transpose_by_step.lua
index 288bfca2..2d2b26b7 100644
--- a/src/transpose_by_step.lua
+++ b/src/transpose_by_step.lua
@@ -41,7 +41,7 @@ function plugindef(locale)
     }
     loc.de = {
         menu = "Transponieren nach Schritten",
-        desc = "Transponieren Sie nach der angegebenen Anzahl von Schritten und vereinfachen Sie die Notation nach Bedarf.",
+        desc = "Transponieren nach der angegebenen Anzahl von Schritten und vereinfachen die Notation nach Bedarf.",
     }
     local t = locale and loc[locale:sub(1,2)] or loc.en
     return t.menu .. "...", t.menu, t.desc
@@ -57,12 +57,38 @@ end
 
 local transposition = require("library.transposition")
 local mixin = require("library.mixin")
+local loc = require("library.localization")
+local utils = require("library.utils")
+
+loc.en = loc.en or {
+    ["Finale is unable to represent some of the transposed pitches. These pitches were left at their original value."] =
+        "Finale is unable to represent some of the transposed pitches. These pitches were left at their original value.",
+    ["Number Of Steps"] = "Number Of Steps",
+    ["Transpose By Steps"] = "Transpose By Steps",
+    ["Transposition Error"] = "Transposition Error",
+}
+
+loc.es = loc.es or {
+    ["Finale is unable to represent some of the transposed pitches. These pitches were left at their original value."] =
+        "Finale no puede representar algunas de las notas transpuestas. Estas notas se dejaron en su valor original.",
+    ["Number Of Steps"] = "Número De Pasos",
+    ["Transpose By Steps"] = "Transponer Por Pasos",
+    ["Transposition Error"] = "Error De Transposición",
+}
+
+loc.de = loc.de or {
+    ["Finale is unable to represent some of the transposed pitches. These pitches were left at their original value."] =
+        "Finale kann einige der transponierten Töne nicht darstellen. Diese Töne wurden auf ihren ursprünglichen Wert belassen.",
+    ["Number Of Steps"] = "Anzahl der Schritte",
+    ["Transpose By Steps"] = "Transponieren nach Schritten",
+    ["Transposition Error"] = "Transpositionsfehler",
+}
 
 function do_transpose_by_step(global_number_of_steps_edit)
     if finenv.Region():IsEmpty() then
         return
     end
-    local undostr = "Transpose By Steps " .. tostring(finenv.Region().StartMeasure)
+    local undostr = loc.localize("Transpose By Steps") .. " " .. tostring(finenv.Region().StartMeasure)
     if finenv.Region().StartMeasure ~= finenv.Region().EndMeasure then
         undostr = undostr .. " - " .. tostring(finenv.Region().EndMeasure)
     end
@@ -80,18 +106,22 @@ function do_transpose_by_step(global_number_of_steps_edit)
         finenv.StartNewUndoBlock(undostr, true) -- JW Lua automatically terminates the final undo block we start here
     end
     if not success then
-        finenv.UI():AlertError("Finale is unable to represent some of the transposed pitches. These pitches were left at their original value.", "Transposition Error")
+        finenv.UI():AlertError(
+            loc.localize(
+                "Finale is unable to represent some of the transposed pitches. These pitches were left at their original value."),
+            loc.localize("Transposition Error")
+        )
     end
     return success
 end
 
 function create_dialog_box()
-    local dialog = mixin.FCXCustomLuaWindow():SetTitle("Transpose By Steps")
+    local dialog = mixin.FCXCustomLuaWindow():SetTitle(loc.localize("Transpose By Steps"))
     local current_y = 0
     local x_increment = 105
     -- number of steps
-    dialog:CreateStatic(0, current_y + 2):SetText("Number Of Steps:")
-    local edit_x = x_increment + (finenv.UI():IsOnMac() and 4 or 0)
+    dialog:CreateStatic(0, current_y + 2):SetText(loc.localize("Number Of Steps"))
+    local edit_x = x_increment + utils.win_mac(0, 4)
     dialog:CreateEdit(edit_x, current_y, "num_steps"):SetText("")
     -- ok/cancel
     dialog:CreateOkButton()

From 77faaa6599719098d88fdad42c2d5fbd65a49d89 Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Fri, 19 Jan 2024 19:11:32 -0600
Subject: [PATCH 18/61] fix a few typos

---
 src/library/localization_developer.lua | 2 +-
 src/transpose_by_step.lua              | 6 +++---
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/library/localization_developer.lua b/src/library/localization_developer.lua
index dfae8ccd..b2e83539 100644
--- a/src/library/localization_developer.lua
+++ b/src/library/localization_developer.lua
@@ -85,7 +85,7 @@ end
 
 local function make_flat_table_string(lang, t)
     local concat = {}
-    table.insert(concat, "localization." .. lang .. " = "localization." .. lang .. " or {\n")
+    table.insert(concat, "localization." .. lang .. " = localization." .. lang .. " or {\n")
     for k, v in pairsbykeys(t) do
         table.insert(concat, "    [\"" .. tostring(k) .. "\"] = \"" .. tostring(v) .. "\",\n")
     end
diff --git a/src/transpose_by_step.lua b/src/transpose_by_step.lua
index 2d2b26b7..3d53b4c7 100644
--- a/src/transpose_by_step.lua
+++ b/src/transpose_by_step.lua
@@ -70,10 +70,10 @@ loc.en = loc.en or {
 
 loc.es = loc.es or {
     ["Finale is unable to represent some of the transposed pitches. These pitches were left at their original value."] =
-        "Finale no puede representar algunas de las notas transpuestas. Estas notas se dejaron en su valor original.",
+        "Finale no puede representar algunas de las notas traspuestas. Estas notas no se han cambiado.",
     ["Number Of Steps"] = "Número De Pasos",
-    ["Transpose By Steps"] = "Transponer Por Pasos",
-    ["Transposition Error"] = "Error De Transposición",
+    ["Transpose By Steps"] = "Trasponer Por Pasos",
+    ["Transposition Error"] = "Error De Trasposición",
 }
 
 loc.de = loc.de or {

From 478fe45c4b043123c55c198c2761dc088cc0c76e Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Sat, 20 Jan 2024 10:16:29 -0600
Subject: [PATCH 19/61] make compatible with JW Lua

---
 src/transpose_by_step.lua | 65 ++++++++++++++++++++++++---------------
 1 file changed, 41 insertions(+), 24 deletions(-)

diff --git a/src/transpose_by_step.lua b/src/transpose_by_step.lua
index 3d53b4c7..895085cd 100644
--- a/src/transpose_by_step.lua
+++ b/src/transpose_by_step.lua
@@ -60,29 +60,37 @@ local mixin = require("library.mixin")
 local loc = require("library.localization")
 local utils = require("library.utils")
 
-loc.en = loc.en or {
-    ["Finale is unable to represent some of the transposed pitches. These pitches were left at their original value."] =
-        "Finale is unable to represent some of the transposed pitches. These pitches were left at their original value.",
-    ["Number Of Steps"] = "Number Of Steps",
-    ["Transpose By Steps"] = "Transpose By Steps",
-    ["Transposition Error"] = "Transposition Error",
-}
+if finenv.IsRGPLua then
+    loc.en = loc.en or {
+        ["Finale is unable to represent some of the transposed pitches. These pitches were left at their original value."] =
+            "Finale is unable to represent some of the transposed pitches. These pitches were left at their original value.",
+        ["Number Of Steps"] = "Number Of Steps",
+        ["Transpose By Steps"] = "Transpose By Steps",
+        ["Transposition Error"] = "Transposition Error",
+        ["OK"] = "OK",
+        ["Cancel"] = "Cancel",
+    }
 
-loc.es = loc.es or {
-    ["Finale is unable to represent some of the transposed pitches. These pitches were left at their original value."] =
-        "Finale no puede representar algunas de las notas traspuestas. Estas notas no se han cambiado.",
-    ["Number Of Steps"] = "Número De Pasos",
-    ["Transpose By Steps"] = "Trasponer Por Pasos",
-    ["Transposition Error"] = "Error De Trasposición",
-}
+    loc.es = loc.es or {
+        ["Finale is unable to represent some of the transposed pitches. These pitches were left at their original value."] =
+            "Finale no puede representar algunas de las notas traspuestas. Estas notas no se han cambiado.",
+        ["Number Of Steps"] = "Número De Pasos",
+        ["Transpose By Steps"] = "Trasponer Por Pasos",
+        ["Transposition Error"] = "Error De Trasposición",
+        ["OK"] = "Aceptar",
+        ["Cancel"] = "Cancelar",
+    }
 
-loc.de = loc.de or {
-    ["Finale is unable to represent some of the transposed pitches. These pitches were left at their original value."] =
-        "Finale kann einige der transponierten Töne nicht darstellen. Diese Töne wurden auf ihren ursprünglichen Wert belassen.",
-    ["Number Of Steps"] = "Anzahl der Schritte",
-    ["Transpose By Steps"] = "Transponieren nach Schritten",
-    ["Transposition Error"] = "Transpositionsfehler",
-}
+    loc.de = loc.de or {
+        ["Finale is unable to represent some of the transposed pitches. These pitches were left at their original value."] =
+            "Finale kann einige der transponierten Töne nicht darstellen. Diese Töne wurden auf ihren ursprünglichen Wert belassen.",
+        ["Number Of Steps"] = "Anzahl der Schritte",
+        ["Transpose By Steps"] = "Transponieren nach Schritten",
+        ["Transposition Error"] = "Transpositionsfehler",
+        ["OK"] = "OK",
+        ["Cancel"] = "Abbrechen",
+    }
+end
 
 function do_transpose_by_step(global_number_of_steps_edit)
     if finenv.Region():IsEmpty() then
@@ -116,16 +124,25 @@ function do_transpose_by_step(global_number_of_steps_edit)
 end
 
 function create_dialog_box()
-    local dialog = mixin.FCXCustomLuaWindow():SetTitle(loc.localize("Transpose By Steps"))
+    local dialog = mixin.FCXCustomLuaWindow()
+        :SetTitle(loc.localize("Transpose By Steps"))
     local current_y = 0
     local x_increment = 105
     -- number of steps
-    dialog:CreateStatic(0, current_y + 2):SetText(loc.localize("Number Of Steps"))
+    dialog:CreateStatic(0, current_y + 2, "steps_label")
+        :SetText(loc.localize("Number Of Steps"))
+        :fallback_call("DoAutoResizeWidth", nil, true)
     local edit_x = x_increment + utils.win_mac(0, 4)
-    dialog:CreateEdit(edit_x, current_y, "num_steps"):SetText("")
+    dialog:CreateEdit(edit_x, current_y, "num_steps")
+        :SetText("")
+        :fallback_call("AssureNoHorizontalOverlap", nil, dialog:GetControl("steps_label"), 10)
     -- ok/cancel
     dialog:CreateOkButton()
+        :SetText(loc.localize("OK"))
+        :fallback_call("DoAutoResizeWidth", nil, true)
     dialog:CreateCancelButton()
+        :SetText(loc.localize("Cancel"))
+        :fallback_call("DoAutoResizeWidth", nil, true)
     dialog:RegisterHandleOkButtonPressed(function(self)
             do_transpose_by_step(self:GetControl("num_steps"):GetInteger())
         end

From ea5b041052b294bba830adbf0fcbd85fe36392ca Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Sat, 20 Jan 2024 13:29:56 -0600
Subject: [PATCH 20/61] fix typo

---
 src/transpose_by_step.lua | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/src/transpose_by_step.lua b/src/transpose_by_step.lua
index 895085cd..a07b31f7 100644
--- a/src/transpose_by_step.lua
+++ b/src/transpose_by_step.lua
@@ -17,7 +17,7 @@ function plugindef(locale)
 
         If you are using custom key signatures with JW Lua or an early version of RGP Lua, you must create
         a custom_key_sig.config.txt file in a folder called `script_settings` within the same folder as the script.
-        It should contains the following two lines that define the custom key signature you are using. Unfortunately,
+        It should contain the following two lines that define the custom key signature you are using. Unfortunately,
         the JW Lua and early versions of RGP Lua do not allow scripts to read this information from the Finale document.
 
         (This example is for 31-EDO.)
@@ -62,8 +62,8 @@ local utils = require("library.utils")
 
 if finenv.IsRGPLua then
     loc.en = loc.en or {
-        ["Finale is unable to represent some of the transposed pitches. These pitches were left at their original value."] =
-            "Finale is unable to represent some of the transposed pitches. These pitches were left at their original value.",
+        ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
+            "Finale is unable to represent some of the transposed pitches. These pitches were left unchanged.",
         ["Number Of Steps"] = "Number Of Steps",
         ["Transpose By Steps"] = "Transpose By Steps",
         ["Transposition Error"] = "Transposition Error",
@@ -72,7 +72,7 @@ if finenv.IsRGPLua then
     }
 
     loc.es = loc.es or {
-        ["Finale is unable to represent some of the transposed pitches. These pitches were left at their original value."] =
+        ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
             "Finale no puede representar algunas de las notas traspuestas. Estas notas no se han cambiado.",
         ["Number Of Steps"] = "Número De Pasos",
         ["Transpose By Steps"] = "Trasponer Por Pasos",
@@ -82,8 +82,8 @@ if finenv.IsRGPLua then
     }
 
     loc.de = loc.de or {
-        ["Finale is unable to represent some of the transposed pitches. These pitches were left at their original value."] =
-            "Finale kann einige der transponierten Töne nicht darstellen. Diese Töne wurden auf ihren ursprünglichen Wert belassen.",
+        ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
+            "Finale kann einige der transponierten Tönhöhen nicht darstellen. Diese Tönhöhen wurden unverändert gelassen.",
         ["Number Of Steps"] = "Anzahl der Schritte",
         ["Transpose By Steps"] = "Transponieren nach Schritten",
         ["Transposition Error"] = "Transpositionsfehler",
@@ -116,7 +116,7 @@ function do_transpose_by_step(global_number_of_steps_edit)
     if not success then
         finenv.UI():AlertError(
             loc.localize(
-                "Finale is unable to represent some of the transposed pitches. These pitches were left at their original value."),
+                "Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."),
             loc.localize("Transposition Error")
         )
     end

From 9ee3264b8b7edb5e998c27b2e251589ed69f8983 Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Sat, 20 Jan 2024 14:13:39 -0600
Subject: [PATCH 21/61] Fix typos in Spanish text.

---
 src/transpose_by_step.lua | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/transpose_by_step.lua b/src/transpose_by_step.lua
index a07b31f7..73678196 100644
--- a/src/transpose_by_step.lua
+++ b/src/transpose_by_step.lua
@@ -36,8 +36,8 @@ function plugindef(locale)
         desc = "Transpose by the number of steps given, simplifying the note spelling as needed."
     }
     loc.es = {
-        menu = "Transponer Por Pasos",
-        desc = "Transponer por el número de pasos dado, simplificando la enarmonización según sea necesario.",
+        menu = "Trasponer Por Pasos",
+        desc = "Trasponer por el número de pasos dado, simplificando la enarmonización según sea necesario.",
     }
     loc.de = {
         menu = "Transponieren nach Schritten",

From 1788e8cd3e15234b617c53ec2910f2c88edbad27 Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Sat, 20 Jan 2024 17:29:00 -0600
Subject: [PATCH 22/61] localized enharmonic transposition scripts

---
 src/transpose_by_step.lua         |  2 +-
 src/transpose_enharmonic_down.lua | 44 ++++++++++++++++++++++++++++---
 src/transpose_enharmonic_up.lua   | 44 ++++++++++++++++++++++++++++---
 3 files changed, 81 insertions(+), 9 deletions(-)

diff --git a/src/transpose_by_step.lua b/src/transpose_by_step.lua
index 73678196..34f938ce 100644
--- a/src/transpose_by_step.lua
+++ b/src/transpose_by_step.lua
@@ -76,7 +76,7 @@ if finenv.IsRGPLua then
             "Finale no puede representar algunas de las notas traspuestas. Estas notas no se han cambiado.",
         ["Number Of Steps"] = "Número De Pasos",
         ["Transpose By Steps"] = "Trasponer Por Pasos",
-        ["Transposition Error"] = "Error De Trasposición",
+        ["Transposition Error"] = "Error de trasposición",
         ["OK"] = "Aceptar",
         ["Cancel"] = "Cancelar",
     }
diff --git a/src/transpose_enharmonic_down.lua b/src/transpose_enharmonic_down.lua
index 7e9ef95c..c2442091 100644
--- a/src/transpose_enharmonic_down.lua
+++ b/src/transpose_enharmonic_down.lua
@@ -1,4 +1,4 @@
-function plugindef()
+function plugindef(locale)
     finaleplugin.RequireSelection = true
     finaleplugin.Author = "Robert Patterson"
     finaleplugin.Copyright = "CC0 https://creativecommons.org/publicdomain/zero/1.0/"
@@ -26,11 +26,43 @@ function plugindef()
         Later versions of RGP Lua (0.58 or higher) ignore this configuration file (if it exists) and read the correct
         information from the Finale document.
     ]]
-    return "Enharmonic Transpose Down", "Enharmonic Transpose Down",
-           "Transpose down enharmonically all notes in selected regions."
+    local loc = {}
+    loc.en = {
+        menu = "Enharmonic Transpose Down",
+        desc = "Transpose down enharmonically all notes in the selected region."
+    }
+    loc.es = {
+        menu = "Trasposición enarmónica hacia abajo",
+        desc = "Trasponer hacia abajo enarmónicamente todas las notas en la región seleccionada.",
+    }
+    loc.de = {
+        menu = "Enharmonische Transposition nach unten",
+        desc = "Transponieren alle Noten im ausgewählten Abschnitt enharmonisch nach unten.",
+    }
+    local t = locale and loc[locale:sub(1,2)] or loc.en
+    return t.menu, t.menu, t.desc
 end
 
 local transposition = require("library.transposition")
+local loc = require('library.localization')
+
+loc.en = loc.en or {
+    ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
+        "Finale is unable to represent some of the transposed pitches. These pitches were left unchanged.",
+    ["Transposition Error"] = "Transposition Error",
+}
+
+loc.es = loc.es or {
+    ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
+        "Finale no puede representar algunas de las notas traspuestas. Estas notas no se han cambiado.",
+    ["Transposition Error"] = "Error de trasposición",
+}
+
+loc.de = loc.de or {
+    ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
+        "Finale kann einige der transponierten Tönhöhen nicht darstellen. Diese Tönhöhen wurden unverändert gelassen.",
+    ["Transposition Error"] = "Transpositionsfehler",
+}
 
 function transpose_enharmonic_down()
     local success = true
@@ -40,7 +72,11 @@ function transpose_enharmonic_down()
         end
     end
     if not success then
-        finenv.UI():AlertError("Finale is unable to represent some of the transposed pitches. These pitches were left at their original value.", "Transposition Error")
+        finenv.UI():AlertError(
+            loc.localize(
+                "Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."),
+            loc.localize("Transposition Error")
+        )
     end
 end
 
diff --git a/src/transpose_enharmonic_up.lua b/src/transpose_enharmonic_up.lua
index 567d1f3a..dcc5e452 100644
--- a/src/transpose_enharmonic_up.lua
+++ b/src/transpose_enharmonic_up.lua
@@ -1,4 +1,4 @@
-function plugindef()
+function plugindef(locale)
     finaleplugin.RequireSelection = true
     finaleplugin.Author = "Robert Patterson"
     finaleplugin.Copyright = "CC0 https://creativecommons.org/publicdomain/zero/1.0/"
@@ -26,11 +26,43 @@ function plugindef()
         Later versions of RGP Lua (0.58 or higher) ignore this configuration file (if it exists) and read the correct
         information from the Finale document.
     ]]
-    return "Enharmonic Transpose Up", "Enharmonic Transpose Up",
-           "Transpose up enharmonically all notes in selected regions."
+    local loc = {}
+    loc.en = {
+        menu = "Enharmonic Transpose Up",
+        desc = "Transpose up enharmonically all notes in the selected region."
+    }
+    loc.es = {
+        menu = "Trasposición enarmónica hacia arriba",
+        desc = "Trasponer hacia arriba enarmónicamente todas las notas en la región seleccionada.",
+    }
+    loc.de = {
+        menu = "Enharmonische Transposition nach oben",
+        desc = "Transponieren alle Noten im ausgewählten Abschnitt enharmonisch nach oben.",
+    }
+    local t = locale and loc[locale:sub(1,2)] or loc.en
+    return t.menu, t.menu, t.desc
 end
 
 local transposition = require("library.transposition")
+local loc = require('library.localization')
+
+loc.en = loc.en or {
+    ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
+        "Finale is unable to represent some of the transposed pitches. These pitches were left unchanged.",
+    ["Transposition Error"] = "Transposition Error",
+}
+
+loc.es = loc.es or {
+    ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
+        "Finale no puede representar algunas de las notas traspuestas. Estas notas no se han cambiado.",
+    ["Transposition Error"] = "Error de trasposición",
+}
+
+loc.de = loc.de or {
+    ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
+        "Finale kann einige der transponierten Tönhöhen nicht darstellen. Diese Tönhöhen wurden unverändert gelassen.",
+    ["Transposition Error"] = "Transpositionsfehler",
+}
 
 function transpose_enharmonic_up()
     local success = true
@@ -40,7 +72,11 @@ function transpose_enharmonic_up()
         end
     end
     if not success then
-        finenv.UI():AlertError("Finale is unable to represent some of the transposed pitches. These pitches were left at their original value.", "Transposition Error")
+        finenv.UI():AlertError(
+            loc.localize(
+                "Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."),
+            loc.localize("Transposition Error")
+        )
     end
 end
 

From f2a8a925af68d7b9842f60c34156fe237a671782 Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Sun, 21 Jan 2024 09:11:42 -0600
Subject: [PATCH 23/61] refinements to localization

---
 src/library/localization.lua | 41 ++++++++++++++++++++++++++----------
 src/transpose_by_step.lua    | 21 ++++++++----------
 2 files changed, 39 insertions(+), 23 deletions(-)

diff --git a/src/library/localization.lua b/src/library/localization.lua
index 0b6338d5..78f8b896 100644
--- a/src/library/localization.lua
+++ b/src/library/localization.lua
@@ -1,14 +1,17 @@
 --[[
 $module Localization
 
-This library provides localization services to scripts. To use it, scripts must define each localization
+This library provides localization services to scripts. Note that this library cannot be used inside
+a `plugindef` function, because the Lua plugin does not load any dependencies when it calls `plugindef`.
+
+To use the library, scripts must define each localization
 as a table appended to this library table. If you provide region-specific localizations, you should also
 provide a generic localization for the 2-character language code as a fallback.
 
 ```
 local localization = require("library.localization")
 --
--- append localizations to the library table:
+-- append localizations to the table returned by `require`:
 --
 localization.en = localization.en or {
     ["Hello"] = "Hello",
@@ -19,13 +22,13 @@ localization.en = localization.en or {
 localization.es = localization.es or {
     ["Hello"] = "Hola",
     ["Goodbye"] = "Adiós",
-    ["Computer"] = "Ordenador"
+    ["Computer"] = "Computadora"
 }
 
--- specific localization for Mexico
--- it is only necessary to specify items that are different from the fallback language table.
-localization.es_MX = localization.es_MX or {
-    ["Computer"] = "Computadora"
+-- specific localization for Spain
+-- it is only necessary to specify items that are different than the fallback language table.
+localization.es_ES = localization.es_ES or {
+    ["Computer"] = "Ordenador"
 }
 
 localization.jp = localization.jp or {
@@ -39,7 +42,7 @@ The keys do not have to be in English, but they should be the same in all tables
 in your script or include them with `require`. Example:
 
 ```
-local region_code = "de_CH" -- get this from `finenv.UI():GetUserLocaleName(): you could also use just the language code "de"
+local region_code = "de_CH" -- get this from `finenv.UI():GetUserLocaleName(): you could also strip out just the language code "de"
 local localization_table_name = "localization_" region_code
 localization[region_code] = require(localization_table_name)
 ```
@@ -60,14 +63,15 @@ local locale = (function()
             finenv.UI():GetUserLocaleName(fcstr)
             return fcstr.LuaString:gsub("-", "_")
         end
-        return nil
+        return "en_US"
     end)()
 
 --[[
 % set_locale
 
 Sets the locale to a specified value. By default, the locale language is the same value as finenv.UI():GetUserLocaleName.
-If you are running a version of Finale Lua that does not have GetUserLocaleName, you must manually set the locale from your script.
+If you are running a version of Finale Lua that does not have GetUserLocaleName, you can either manually set the locale
+from your script or accept the default, "en_US".
 
 This function can also be used to test different localizations without the need to switch user preferences in the OS.
 
@@ -77,6 +81,19 @@ function localization.set_locale(input_locale)
     locale = input_locale:gsub("-", "_")
 end
 
+--[[
+% get_locale
+
+Returns the locale value that the localization library is using. Normally it matches the value returned by
+`finenv.UI():GetUserLocaleName`, however it returns a value in any Lua plugin version including JW Lua.
+
+: (string) the current locale string that the localization library is using
+]]
+function localization.get_locale()
+    return locale
+end
+
+
 --[[
 % localize
 
@@ -88,7 +105,9 @@ Localizes a string based on the localization language
 function localization.localize(input_string)
     assert(type(input_string) == "string", "expected string, got " .. type(input_string))
 
-    if not locale then return input_string end
+    if locale == nil then
+        return input_string
+    end
     assert(type(locale) == "string", "invalid locale setting " .. tostring(locale))
     
     local t = localization[locale]
diff --git a/src/transpose_by_step.lua b/src/transpose_by_step.lua
index 34f938ce..c4832c16 100644
--- a/src/transpose_by_step.lua
+++ b/src/transpose_by_step.lua
@@ -65,7 +65,6 @@ if finenv.IsRGPLua then
         ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
             "Finale is unable to represent some of the transposed pitches. These pitches were left unchanged.",
         ["Number Of Steps"] = "Number Of Steps",
-        ["Transpose By Steps"] = "Transpose By Steps",
         ["Transposition Error"] = "Transposition Error",
         ["OK"] = "OK",
         ["Cancel"] = "Cancel",
@@ -75,7 +74,6 @@ if finenv.IsRGPLua then
         ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
             "Finale no puede representar algunas de las notas traspuestas. Estas notas no se han cambiado.",
         ["Number Of Steps"] = "Número De Pasos",
-        ["Transpose By Steps"] = "Trasponer Por Pasos",
         ["Transposition Error"] = "Error de trasposición",
         ["OK"] = "Aceptar",
         ["Cancel"] = "Cancelar",
@@ -85,7 +83,6 @@ if finenv.IsRGPLua then
         ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
             "Finale kann einige der transponierten Tönhöhen nicht darstellen. Diese Tönhöhen wurden unverändert gelassen.",
         ["Number Of Steps"] = "Anzahl der Schritte",
-        ["Transpose By Steps"] = "Transponieren nach Schritten",
         ["Transposition Error"] = "Transpositionsfehler",
         ["OK"] = "OK",
         ["Cancel"] = "Abbrechen",
@@ -96,7 +93,7 @@ function do_transpose_by_step(global_number_of_steps_edit)
     if finenv.Region():IsEmpty() then
         return
     end
-    local undostr = loc.localize("Transpose By Steps") .. " " .. tostring(finenv.Region().StartMeasure)
+    local undostr = ({plugindef(loc.get_locale())})[2] .. " " .. tostring(finenv.Region().StartMeasure)
     if finenv.Region().StartMeasure ~= finenv.Region().EndMeasure then
         undostr = undostr .. " - " .. tostring(finenv.Region().EndMeasure)
     end
@@ -125,28 +122,28 @@ end
 
 function create_dialog_box()
     local dialog = mixin.FCXCustomLuaWindow()
-        :SetTitle(loc.localize("Transpose By Steps"))
+        :SetTitle(plugindef(loc.get_locale()):gsub("%.%.%.", ""))
     local current_y = 0
     local x_increment = 105
     -- number of steps
     dialog:CreateStatic(0, current_y + 2, "steps_label")
         :SetText(loc.localize("Number Of Steps"))
-        :fallback_call("DoAutoResizeWidth", nil, true)
+        :_FallbackCall("DoAutoResizeWidth", nil, true)
     local edit_x = x_increment + utils.win_mac(0, 4)
     dialog:CreateEdit(edit_x, current_y, "num_steps")
         :SetText("")
-        :fallback_call("AssureNoHorizontalOverlap", nil, dialog:GetControl("steps_label"), 10)
+        :_FallbackCall("AssureNoHorizontalOverlap", nil, dialog:GetControl("steps_label"), 10)
     -- ok/cancel
     dialog:CreateOkButton()
         :SetText(loc.localize("OK"))
-        :fallback_call("DoAutoResizeWidth", nil, true)
+        :_FallbackCall("DoAutoResizeWidth", nil, true)
     dialog:CreateCancelButton()
         :SetText(loc.localize("Cancel"))
-        :fallback_call("DoAutoResizeWidth", nil, true)
+        :_FallbackCall("DoAutoResizeWidth", nil, true)
+    -- registrations
     dialog:RegisterHandleOkButtonPressed(function(self)
-            do_transpose_by_step(self:GetControl("num_steps"):GetInteger())
-        end
-    )
+        do_transpose_by_step(self:GetControl("num_steps"):GetInteger())
+    end)
     return dialog
 end
 

From 779811d6860636e9d9b3f22db73bd97acbfdbe95 Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Sun, 21 Jan 2024 11:12:14 -0600
Subject: [PATCH 24/61] add localization for chromatic transpose

---
 src/transpose_by_step.lua | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/transpose_by_step.lua b/src/transpose_by_step.lua
index c4832c16..a684eadf 100644
--- a/src/transpose_by_step.lua
+++ b/src/transpose_by_step.lua
@@ -36,7 +36,7 @@ function plugindef(locale)
         desc = "Transpose by the number of steps given, simplifying the note spelling as needed."
     }
     loc.es = {
-        menu = "Trasponer Por Pasos",
+        menu = "Trasponer por pasos",
         desc = "Trasponer por el número de pasos dado, simplificando la enarmonización según sea necesario.",
     }
     loc.de = {
@@ -132,7 +132,7 @@ function create_dialog_box()
     local edit_x = x_increment + utils.win_mac(0, 4)
     dialog:CreateEdit(edit_x, current_y, "num_steps")
         :SetText("")
-        :_FallbackCall("AssureNoHorizontalOverlap", nil, dialog:GetControl("steps_label"), 10)
+        :_FallbackCall("AssureNoHorizontalOverlap", nil, dialog:GetControl("steps_label"), 5)
     -- ok/cancel
     dialog:CreateOkButton()
         :SetText(loc.localize("OK"))

From 3e11d5cb8ca88e3a3b1fa6513e2b240179bfa114 Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Sun, 21 Jan 2024 15:36:51 -0600
Subject: [PATCH 25/61] refinements to localizing transposition

---
 samples/auto_layout.lua   | 16 +++++++++++++---
 src/transpose_by_step.lua |  1 +
 2 files changed, 14 insertions(+), 3 deletions(-)

diff --git a/samples/auto_layout.lua b/samples/auto_layout.lua
index 653b775a..e9bcec36 100644
--- a/samples/auto_layout.lua
+++ b/samples/auto_layout.lua
@@ -16,6 +16,7 @@ localization.en = -- this is en_GB due to spelling of "Localisation"
 {
     ["Action Button"] = "Action Button",
     ["Choices"] = "Choices",
+    ["Close"] = "Close",
     ["First Option"] = "First Option",
     ["Fourth Option"] = "Fourth Option",
     ["Left Checkbox Option 1"] = "Left Checkbox Option 1",
@@ -44,6 +45,7 @@ localization.en_US =
 localization.es = {
     ["Action Button"] = "Botón de Acción",
     ["Choices"] = "Opciones",
+    ["Close"] = "Cerrar",
     ["First Option"] = "Primera Opción",
     ["Fourth Option"] = "Cuarta Opción",
     ["Left Checkbox Option 1"] = "Opción de Casilla de Verificación Izquierda 1",
@@ -65,6 +67,7 @@ localization.es = {
 localization.jp = {
     ["Action Button"] = "アクションボタン",
     ["Choices"] = "選択肢",
+    ["Close"] = "閉じる",
     ["First Option"] = "最初のオプション",
     ["Fourth Option"] = "第四のオプション",
     ["Left Checkbox Option 1"] = "左チェックボックスオプション1",
@@ -86,6 +89,7 @@ localization.jp = {
 localization.de = {
     ["Action Button"] = "Aktionsknopf",
     ["Choices"] = "Auswahlmöglichkeiten",
+    ["Close"] = "Schließen",
     ["First Option"] = "Erste Option",
     ["Fourth Option"] = "Vierte Option",
     ["Left Checkbox Option 1"] = "Linke Checkbox Option 1",
@@ -104,6 +108,7 @@ localization.de = {
 localization.fr = {
     ["Action Button"] = "Bouton d'action",
     ["Choices"] = "Choix",
+    ["Close"] = "Close",
     ["First Option"] = "Première Option",
     ["Fourth Option"] = "Quatrième Option",
     ["Left Checkbox Option 1"] = "Option de case à cocher gauche 1",
@@ -122,6 +127,7 @@ localization.fr = {
 localization.zh = {
     ["Action Button"] = "操作按钮",
     ["Choices"] = "选择:",
+    ["Close"] = "关闭",
     ["First Option"] = "第一选项:",
     ["Fourth Option"] = "第四选项:",
     ["Left Checkbox Option 1"] = "左侧复选框选项1",
@@ -140,6 +146,7 @@ localization.zh = {
 localization.ar = {
     ["Action Button"] = "زر العمل",
     ["Choices"] = "الخيارات",
+    ["Close"] = "إغلاق",
     ["First Option"] = "الخيار الأول",
     ["Fourth Option"] = "الخيار الرابع",
     ["Left Checkbox Option 1"] = "خيار المربع الأول على اليسار",
@@ -158,6 +165,7 @@ localization.ar = {
 localization.fa = {
     ["Action Button"] = "دکمه عملیات",
     ["Choices"] = "گزینه ها",
+    ["Close"] = "بستن",
     ["First Option"] = "گزینه اول",
     ["Fourth Option"] = "گزینه چهارم",
     ["Left Checkbox Option 1"] = "گزینه چک باکس سمت چپ 1",
@@ -173,7 +181,7 @@ localization.fa = {
     ["This is longer option text "] = "این متن گزینه طولانی تر است ",
 }
 
---localization.set_locale("fa")
+localization.set_locale("fa")
 
 function create_dialog()
     local dlg = mixin.FCXCustomLuaWindow()
@@ -205,7 +213,7 @@ function create_dialog()
         :DoAutoResizeWidth(true)
         :SetWidth(0)
         :SetText(localization.localize("Second Option"))
-    dlg:CreateEdit(0, line_no * y_increment - utils.win_mac(2, 3), "option2")
+    dlg:CreateEdit(10, line_no * y_increment - utils.win_mac(2, 3), "option2")
         :SetInteger(2)
         :AssureNoHorizontalOverlap(dlg:GetControl("option2-label"), label_edit_separ)
         :HorizontallyAlignLeftWith(dlg:GetControl("option1"))
@@ -261,6 +269,7 @@ function create_dialog()
         :SetText(localization.localize("Action Button"))
         :AssureNoHorizontalOverlap(vertical_line, center_padding)
         :HorizontallyAlignRightWith(dlg:GetControl("option4"))
+--        :HorizontallyAlignRightWithFurthest()
     line_no = line_no + 1
 
     -- horizontal line here
@@ -325,8 +334,9 @@ function create_dialog()
     line_no = line_no + 2
 
     dlg:CreateCloseButton(0, line_no * y_increment + 5)
+        :SetText(localization.localize("Close"))
+        :DoAutoResizeWidth(true)
         :HorizontallyAlignRightWithFurthest()
-        :DoAutoResizeWidth()
 
     return dlg
 end
diff --git a/src/transpose_by_step.lua b/src/transpose_by_step.lua
index a684eadf..a586a917 100644
--- a/src/transpose_by_step.lua
+++ b/src/transpose_by_step.lua
@@ -128,6 +128,7 @@ function create_dialog_box()
     -- number of steps
     dialog:CreateStatic(0, current_y + 2, "steps_label")
         :SetText(loc.localize("Number Of Steps"))
+        :SetWidth(x_increment - 5)
         :_FallbackCall("DoAutoResizeWidth", nil, true)
     local edit_x = x_increment + utils.win_mac(0, 4)
     dialog:CreateEdit(edit_x, current_y, "num_steps")

From 1e608724037010261559c6683f0b56cc76ec5bb7 Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Mon, 22 Jan 2024 08:22:25 -0600
Subject: [PATCH 26/61] auto_layout changes for latest RGP Lua dev branch

---
 samples/auto_layout.lua | 28 ++++++++++++++--------------
 1 file changed, 14 insertions(+), 14 deletions(-)

diff --git a/samples/auto_layout.lua b/samples/auto_layout.lua
index e9bcec36..22e87a87 100644
--- a/samples/auto_layout.lua
+++ b/samples/auto_layout.lua
@@ -195,7 +195,7 @@ function create_dialog()
 
     -- left side
     dlg:CreateStatic(0, line_no * y_increment, "option1-label")
-        :DoAutoResizeWidth(true)
+        :DoAutoResizeWidth()
         :SetWidth(0)
         :SetText(localization.localize("First Option"))
     dlg:CreateEdit(0, line_no * y_increment - utils.win_mac(2, 3), "option1")
@@ -204,13 +204,13 @@ function create_dialog()
     line_no = line_no + 1
 
     dlg:CreateCheckbox(0, line_no * y_increment, "left-checkbox1")
-        :DoAutoResizeWidth(true)
+        :DoAutoResizeWidth()
         :SetWidth(0)
         :SetText(localization.localize("Left Checkbox Option 1"))
     line_no = line_no + 1
 
     dlg:CreateStatic(0, line_no * y_increment, "option2-label")
-        :DoAutoResizeWidth(true)
+        :DoAutoResizeWidth()
         :SetWidth(0)
         :SetText(localization.localize("Second Option"))
     dlg:CreateEdit(10, line_no * y_increment - utils.win_mac(2, 3), "option2")
@@ -220,7 +220,7 @@ function create_dialog()
     line_no = line_no + 1
 
     dlg:CreateCheckbox(0, line_no * y_increment, "left-checkbox2")
-        :DoAutoResizeWidth(true)
+        :DoAutoResizeWidth()
         :SetWidth(0)
         :SetText(localization.localize("Left Checkbox Option 2"))
     line_no = line_no + 1
@@ -235,7 +235,7 @@ function create_dialog()
 
     -- right side
     dlg:CreateStatic(0, line_no * y_increment, "option3-label")
-        :DoAutoResizeWidth(true)
+        :DoAutoResizeWidth()
         :SetWidth(0)
         :SetText(localization.localize("Third Option"))
         :AssureNoHorizontalOverlap(vertical_line, center_padding)
@@ -245,7 +245,7 @@ function create_dialog()
     line_no = line_no + 1
 
     dlg:CreateCheckbox(0, line_no * y_increment, "right-checkbox1")
-        :DoAutoResizeWidth(true)
+        :DoAutoResizeWidth()
         :SetWidth(0)
         :SetText(localization.localize("Right Three-State Option"))
         :SetThreeStatesMode(true)
@@ -253,7 +253,7 @@ function create_dialog()
     line_no = line_no + 1
 
     dlg:CreateStatic(0, line_no * y_increment, "option4-label")
-        :DoAutoResizeWidth(true)
+        :DoAutoResizeWidth()
         :SetWidth(0)
         :SetText(localization.localize("Fourth Option"))
         :AssureNoHorizontalOverlap(vertical_line, center_padding)
@@ -264,7 +264,7 @@ function create_dialog()
     line_no = line_no + 1
 
     dlg:CreateButton(0, line_no * y_increment)
-        :DoAutoResizeWidth(true)
+        :DoAutoResizeWidth()
         :SetWidth(0)
         :SetText(localization.localize("Action Button"))
         :AssureNoHorizontalOverlap(vertical_line, center_padding)
@@ -280,11 +280,11 @@ function create_dialog()
     -- bottom side
     local start_line_no = line_no
     dlg:CreateStatic(0, line_no * y_increment, "popup_label")
-        :DoAutoResizeWidth(true)
+        :DoAutoResizeWidth()
         :SetWidth(0)
         :SetText(localization.localize("Menu"))
     local ctrl_popup = dlg:CreatePopup(0, line_no * y_increment - utils.win_mac(2, 2), "popup")
-        :DoAutoResizeWidth(true)
+        :DoAutoResizeWidth()
         :SetWidth(0)
         :AssureNoHorizontalOverlap(dlg:GetControl("popup_label"), label_edit_separ)
     for counter = 1, 3 do
@@ -298,11 +298,11 @@ function create_dialog()
     line_no = line_no + 1
 
     dlg:CreateStatic(0, line_no * y_increment, "cbobox_label")
-        :DoAutoResizeWidth(true)
+        :DoAutoResizeWidth()
         :SetWidth(0)
         :SetText(localization.localize("Choices"))
     local ctrl_cbobox = dlg:CreateComboBox(0, line_no * y_increment - utils.win_mac(2, 3), "cbobox")
-        :DoAutoResizeWidth(true)
+        :DoAutoResizeWidth()
         :SetWidth(40)
         :AssureNoHorizontalOverlap(dlg:GetControl("cbobox_label"), label_edit_separ)
         :HorizontallyAlignLeftWith(ctrl_popup)
@@ -321,7 +321,7 @@ function create_dialog()
     local counter = 1
     for rbtn in each(ctrl_radiobuttons) do
         rbtn:SetWidth(0)
-            :DoAutoResizeWidth(true)
+            :DoAutoResizeWidth()
             :AssureNoHorizontalOverlap(ctrl_popup, 10)
             :AssureNoHorizontalOverlap(ctrl_cbobox, 10)
         if counter == 2 then
@@ -335,7 +335,7 @@ function create_dialog()
 
     dlg:CreateCloseButton(0, line_no * y_increment + 5)
         :SetText(localization.localize("Close"))
-        :DoAutoResizeWidth(true)
+        :DoAutoResizeWidth()
         :HorizontallyAlignRightWithFurthest()
 
     return dlg

From a0f6c217634385ce13e7c133eb801e530e704b9b Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Tue, 23 Jan 2024 08:34:07 -0600
Subject: [PATCH 27/61] Add mixin version of `__FCUserWindow::CreateChildUI`
 for backwards compatibility with JW Lua.

---
 src/mixin/__FCMUserWindow.lua | 19 +++++++++++++++++++
 src/transpose_by_step.lua     |  2 +-
 2 files changed, 20 insertions(+), 1 deletion(-)

diff --git a/src/mixin/__FCMUserWindow.lua b/src/mixin/__FCMUserWindow.lua
index 51e023bd..8310b55c 100644
--- a/src/mixin/__FCMUserWindow.lua
+++ b/src/mixin/__FCMUserWindow.lua
@@ -60,4 +60,23 @@ function methods:SetTitle(title)
     self:SetTitle__(mixin_helper.to_fcstring(title, temp_str))
 end
 
+--[[
+% CreateChildUI
+
+**[Override]**
+
+Override Changes:
+- Returns original `CreateChildUI` if the method exists, otherwise it returns `mixin.UI()`
+
+@ self (__FCMUserWindow)
+: (FCMUI)
+]]
+function methods:CreateChildUI()
+    if self.CreateChildUI__ then
+        return self:CreateChildUI__()
+    end
+
+    return mixin.UI()
+end
+
 return class
diff --git a/src/transpose_by_step.lua b/src/transpose_by_step.lua
index a586a917..da553e63 100644
--- a/src/transpose_by_step.lua
+++ b/src/transpose_by_step.lua
@@ -111,7 +111,7 @@ function do_transpose_by_step(global_number_of_steps_edit)
         finenv.StartNewUndoBlock(undostr, true) -- JW Lua automatically terminates the final undo block we start here
     end
     if not success then
-        finenv.UI():AlertError(
+        global_dialog:CreateChildUI():AlertError(
             loc.localize(
                 "Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."),
             loc.localize("Transposition Error")

From f5686807fbb79706ad34cc1c463c1b4ea8cb9032 Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Mon, 29 Jan 2024 07:32:49 -0600
Subject: [PATCH 28/61] update finenv with latest info

---
 docs/rgp-lua/finenv-properties.md | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/docs/rgp-lua/finenv-properties.md b/docs/rgp-lua/finenv-properties.md
index 16698e0e..ce80ab3f 100644
--- a/docs/rgp-lua/finenv-properties.md
+++ b/docs/rgp-lua/finenv-properties.md
@@ -245,7 +245,7 @@ end
 
 A list of constants that define the type of message returned by `finenv.ExecuteLuaScriptItem` (if any).
 
-- `SCRIPT_RESULT` : The message was returned by Lua. It could be either an error or a value returned by the script. If it is an error, the first value returned by `ExecuteLuaScriptItem` is false.|
+- `SCRIPT_RESULT` : The message was returned by the Lua script. This is not an error message.
 - `DOCUMENT_REQUIRED` : The script was not executed because it specified `finaleplugin.RequireDocument = true` but no document was open.
 - `SELECTION_REQUIRED` : The script was not executed because it specified `finaleplugin.RequireSelection = true` but there was no selection.
 - `SCORE_REQUIRED` : The script was not executed because it specified `finaleplugin.RequireScore = true` but the document was viewing a part.
@@ -253,6 +253,7 @@ A list of constants that define the type of message returned by `finenv.ExecuteL
 - `LUA_PLUGIN_VERSION_MISMATCH` : The script was not executed because it specified a minimum or maximum Lua plugin version and the current running version of _RGP Lua_ does not meet the requirement.
 - `MISCELLANEOUS` : Other types of error messages that do not fit any of the other categories.
 - `EXTERNAL_TERMINATION` : The script was externally terminated by the user or a controlling script.
+- `LUA_ERROR` : The message is an error message returned by Lua.
 
 Example:
 
@@ -260,7 +261,7 @@ Example:
 local scripts = finenv.CreateLuaScriptItems()
 local success, error_msg, msg_type = finenv.ExecuteLuaScriptItem(scripts:GetItemAt(0))
 if not success then
-   if msg_type == finenv.MessageResultType.SCRIPT_RESULT then
+   if msg_type == finenv.MessageResultType.LUA_ERROR then
       -- take some action
    end
 end

From f0c0383e72e4bdab7b750c5f9b9dca68b7be8989 Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Tue, 30 Jan 2024 22:47:23 -0600
Subject: [PATCH 29/61] one more lint warning

---
 src/library/localization_developer.lua | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/library/localization_developer.lua b/src/library/localization_developer.lua
index b2e83539..aed2ca3d 100644
--- a/src/library/localization_developer.lua
+++ b/src/library/localization_developer.lua
@@ -36,7 +36,6 @@ function localization_developer.create_localized_base_table()
     file_path = client.encode_with_client_codepage(file_path)
     local file = io.open(file_path, "r")
     if file then
-        local file_content = file:read("all")
         local function extract_strings(file_content)
             local i = 1
             local length = #file_content
@@ -75,6 +74,7 @@ function localization_developer.create_localized_base_table()
                 return nil
             end
         end
+        local file_content = file:read("all")
         for found_string in extract_strings(file_content) do
             retval[found_string] = found_string
         end

From 5a78f36faf726326c0776b704a5c6f9e4292156b Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Thu, 1 Feb 2024 10:22:17 -0600
Subject: [PATCH 30/61] transpose_by_step is working

---
 src/library/localization.lua                | 25 +++++++++++-
 src/localization/transpose_by_step_de.lua   | 13 ++++++
 src/localization/transpose_by_step_es.lua   | 13 ++++++
 src/localization/transpose_chromatic_es.lua | 13 ++++++
 src/mixin/FCMControl.lua                    | 18 +++++++++
 src/mixin/FCMUI.lua                         | 20 ++++++++++
 src/transpose_by_step.lua                   | 44 ++++-----------------
 7 files changed, 107 insertions(+), 39 deletions(-)
 create mode 100644 src/localization/transpose_by_step_de.lua
 create mode 100644 src/localization/transpose_by_step_es.lua
 create mode 100644 src/localization/transpose_chromatic_es.lua

diff --git a/src/library/localization.lua b/src/library/localization.lua
index 78f8b896..ef1d534a 100644
--- a/src/library/localization.lua
+++ b/src/library/localization.lua
@@ -57,6 +57,8 @@ copy into scripts. You can then edit them to suit your needs.
 
 local localization = {}
 
+local library = require("library.general_library")
+
 local locale = (function()
         if finenv.UI().GetUserLocaleName then
             local fcstr = finale.FCString()
@@ -66,6 +68,8 @@ local locale = (function()
         return "en_US"
     end)()
 
+local script_name = library.calc_script_name()
+
 --[[
 % set_locale
 
@@ -93,6 +97,23 @@ function localization.get_locale()
     return locale
 end
 
+-- This function finds a localization string table if it exists or requires it if it doesn't.
+local function get_localized_table(try_locale)
+    if type(localization[try_locale]) == "table" then
+        return localization[try_locale]
+    end
+    local require_library = "localization" .. "." .. script_name .. "_" .. try_locale
+    local success, result = pcall(function() return require(require_library) end)
+    if success and type(result) == "table" then
+        localization[try_locale] = result
+    else
+        print("unable to require " .. require_library)
+        print(result)
+        -- doing this allows us to only try to require it once
+        localization[try_locale] = {}
+    end
+    return localization[try_locale]
+end
 
 --[[
 % localize
@@ -110,13 +131,13 @@ function localization.localize(input_string)
     end
     assert(type(locale) == "string", "invalid locale setting " .. tostring(locale))
     
-    local t = localization[locale]
+    local t = get_localized_table(locale)
     if t and t[input_string] then
         return t[input_string]
     end
 
     if #locale > 2 then
-        t = localization[locale:sub(1, 2)]
+        t = get_localized_table(locale:sub(1, 2))
     end
     
     return t and t[input_string] or input_string
diff --git a/src/localization/transpose_by_step_de.lua b/src/localization/transpose_by_step_de.lua
new file mode 100644
index 00000000..804051b9
--- /dev/null
+++ b/src/localization/transpose_by_step_de.lua
@@ -0,0 +1,13 @@
+--[[
+    German localization for transpose_by_step.lua
+]]
+local loc = {
+    ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
+        "Finale kann einige der transponierten Tönhöhen nicht darstellen. Diese Tönhöhen wurden unverändert gelassen.",
+    ["Number Of Steps"] = "Anzahl der Schritte",
+    ["Transposition Error"] = "Transpositionsfehler",
+    ["OK"] = "OK",
+    ["Cancel"] = "Abbrechen",
+}
+
+return loc
diff --git a/src/localization/transpose_by_step_es.lua b/src/localization/transpose_by_step_es.lua
new file mode 100644
index 00000000..f1cc196b
--- /dev/null
+++ b/src/localization/transpose_by_step_es.lua
@@ -0,0 +1,13 @@
+--[[
+    Spanish localization for transpose_by_step.lua
+]]
+local loc = {
+    ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
+        "Finale no puede representar algunas de las notas traspuestas. Estas notas no se han cambiado.",
+    ["Number Of Steps"] = "Número De Pasos",
+    ["Transposition Error"] = "Error de trasposición",
+    ["OK"] = "Aceptar",
+    ["Cancel"] = "Cancelar",
+}
+
+return loc
diff --git a/src/localization/transpose_chromatic_es.lua b/src/localization/transpose_chromatic_es.lua
new file mode 100644
index 00000000..f1cc196b
--- /dev/null
+++ b/src/localization/transpose_chromatic_es.lua
@@ -0,0 +1,13 @@
+--[[
+    Spanish localization for transpose_by_step.lua
+]]
+local loc = {
+    ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
+        "Finale no puede representar algunas de las notas traspuestas. Estas notas no se han cambiado.",
+    ["Number Of Steps"] = "Número De Pasos",
+    ["Transposition Error"] = "Error de trasposición",
+    ["OK"] = "Aceptar",
+    ["Cancel"] = "Cancelar",
+}
+
+return loc
diff --git a/src/mixin/FCMControl.lua b/src/mixin/FCMControl.lua
index 7e868471..2884619f 100644
--- a/src/mixin/FCMControl.lua
+++ b/src/mixin/FCMControl.lua
@@ -13,6 +13,8 @@ $module FCMControl
 local mixin = require("library.mixin")
 local mixin_helper = require("library.mixin_helper")
 
+local localization = require("library.localization")
+
 local class = {Methods = {}}
 local methods = class.Methods
 local private = setmetatable({}, {__mode = "k"})
@@ -395,4 +397,20 @@ Removes a handler added with `AddHandleCommand`.
 ]]
 methods.AddHandleCommand, methods.RemoveHandleCommand = mixin_helper.create_standard_control_event("HandleCommand")
 
+--[[
+% SetLocalizedText
+
+**[Fluid]**
+
+Removes a handler added with `AddHandleCommand`.
+
+@ self (FCMControl)
+@ key (string) The key into the localization table. If there is no entry in the appropriate localization table, the key is the text.
+]]
+function methods:SetLocalizedText(key)
+    mixin_helper.assert_argument_type(2, key, "string", "FCString")
+
+    self:SetText(localization.localize(key))
+end
+
 return class
diff --git a/src/mixin/FCMUI.lua b/src/mixin/FCMUI.lua
index 1618991a..a738d1ca 100644
--- a/src/mixin/FCMUI.lua
+++ b/src/mixin/FCMUI.lua
@@ -9,6 +9,8 @@ $module FCMUI
 local mixin = require("library.mixin") -- luacheck: ignore
 local mixin_helper = require("library.mixin_helper")
 
+local localization = require("library.localization")
+
 local class = {Methods = {}}
 local methods = class.Methods
 
@@ -42,4 +44,22 @@ function methods:GetDecimalSeparator(str)
     end
 end
 
+--[[
+% AlertLocalizedError
+
+**[Fluid]**
+
+Displays a localized error message. 
+
+@ self (FCMControl)
+@ message_key (string) The key into the localization table. If there is no entry in the appropriate localization table, the key is the message.
+@ title_key (string) The key into the localization table. If there is no entry in the appropriate localization table, the key is the title.
+]]
+function methods:AlertLocalizedError(message_key, title_key)
+    mixin_helper.assert_argument_type(2, message_key, "string", "FCString")
+    mixin_helper.assert_argument_type(3, title_key, "string", "FCString")
+
+    self:AlertError(localization.localize(message_key), localization.localize(title_key))
+end
+
 return class
diff --git a/src/transpose_by_step.lua b/src/transpose_by_step.lua
index da553e63..8b61859b 100644
--- a/src/transpose_by_step.lua
+++ b/src/transpose_by_step.lua
@@ -60,35 +60,6 @@ local mixin = require("library.mixin")
 local loc = require("library.localization")
 local utils = require("library.utils")
 
-if finenv.IsRGPLua then
-    loc.en = loc.en or {
-        ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
-            "Finale is unable to represent some of the transposed pitches. These pitches were left unchanged.",
-        ["Number Of Steps"] = "Number Of Steps",
-        ["Transposition Error"] = "Transposition Error",
-        ["OK"] = "OK",
-        ["Cancel"] = "Cancel",
-    }
-
-    loc.es = loc.es or {
-        ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
-            "Finale no puede representar algunas de las notas traspuestas. Estas notas no se han cambiado.",
-        ["Number Of Steps"] = "Número De Pasos",
-        ["Transposition Error"] = "Error de trasposición",
-        ["OK"] = "Aceptar",
-        ["Cancel"] = "Cancelar",
-    }
-
-    loc.de = loc.de or {
-        ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
-            "Finale kann einige der transponierten Tönhöhen nicht darstellen. Diese Tönhöhen wurden unverändert gelassen.",
-        ["Number Of Steps"] = "Anzahl der Schritte",
-        ["Transposition Error"] = "Transpositionsfehler",
-        ["OK"] = "OK",
-        ["Cancel"] = "Abbrechen",
-    }
-end
-
 function do_transpose_by_step(global_number_of_steps_edit)
     if finenv.Region():IsEmpty() then
         return
@@ -111,11 +82,10 @@ function do_transpose_by_step(global_number_of_steps_edit)
         finenv.StartNewUndoBlock(undostr, true) -- JW Lua automatically terminates the final undo block we start here
     end
     if not success then
-        global_dialog:CreateChildUI():AlertError(
-            loc.localize(
-                "Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."),
-            loc.localize("Transposition Error")
-        )
+        global_dialog:CreateChildUI():AlertLocalizedError(
+            "Finale is unable to represent some of the transposed pitches. These pitches were left unchanged.",
+            "Transposition Error"
+        )            
     end
     return success
 end
@@ -127,7 +97,7 @@ function create_dialog_box()
     local x_increment = 105
     -- number of steps
     dialog:CreateStatic(0, current_y + 2, "steps_label")
-        :SetText(loc.localize("Number Of Steps"))
+        :SetLocalizedText("Number Of Steps")
         :SetWidth(x_increment - 5)
         :_FallbackCall("DoAutoResizeWidth", nil, true)
     local edit_x = x_increment + utils.win_mac(0, 4)
@@ -136,10 +106,10 @@ function create_dialog_box()
         :_FallbackCall("AssureNoHorizontalOverlap", nil, dialog:GetControl("steps_label"), 5)
     -- ok/cancel
     dialog:CreateOkButton()
-        :SetText(loc.localize("OK"))
+        :SetLocalizedText("OK")
         :_FallbackCall("DoAutoResizeWidth", nil, true)
     dialog:CreateCancelButton()
-        :SetText(loc.localize("Cancel"))
+        :SetLocalizedText("Cancel")
         :_FallbackCall("DoAutoResizeWidth", nil, true)
     -- registrations
     dialog:RegisterHandleOkButtonPressed(function(self)

From 2cf8a3df272f9671ee8f4e75d9052618ce50d27c Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Thu, 1 Feb 2024 10:58:32 -0600
Subject: [PATCH 31/61] transpose_chromatic

---
 src/localization/transpose_chromatic_de.lua |  46 ++++++
 src/localization/transpose_chromatic_es.lua |  37 ++++-
 src/mixin/FCMControl.lua                    |   3 +-
 src/mixin/FCMCtrlPopup.lua                  |  21 +++
 src/mixin/FCMUI.lua                         |   5 +-
 src/transpose_chromatic.lua                 | 167 +++++++++++++-------
 6 files changed, 215 insertions(+), 64 deletions(-)
 create mode 100644 src/localization/transpose_chromatic_de.lua

diff --git a/src/localization/transpose_chromatic_de.lua b/src/localization/transpose_chromatic_de.lua
new file mode 100644
index 00000000..d52dcb3b
--- /dev/null
+++ b/src/localization/transpose_chromatic_de.lua
@@ -0,0 +1,46 @@
+--[[
+    German localization for transpose_by_step.lua
+]]
+local loc = {
+    ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
+        "Finale kann einige der transponierten Tönhöhen nicht darstellen. Diese Tönhöhen wurden unverändert gelassen.",
+    ["Augmented Fifth"] = "Übermäßige Quinte",
+    ["Augmented Fourth"] = "Übermäßige Quarte",
+    ["Augmented Second"] = "Übermäßige Sekunde",
+    ["Augmented Seventh"] = "Übermäßige Septime",
+    ["Augmented Sixth"] = "Übermäßige Sexte",
+    ["Augmented Third"] = "Übermäßige Terz",
+    ["Augmented Unison"] = "Übermäßige Prime",
+    ["Diminished Fifth"] = "Verminderte Quinte",
+    ["Diminished Fourth"] = "Verminderte Quarte",
+    ["Diminished Octave"] = "Verminderte Oktave",
+    ["Diminished Second"] = "Verminderte Sekunde",
+    ["Diminished Seventh"] = "Verminderte Septime",
+    ["Diminished Sixth"] = "Verminderte Sexte",
+    ["Diminished Third"] = "Verminderte Terz",
+    ["Direction"] = "Richtung",
+    ["Down"] = "Runter",
+    ["Interval"] = "Intervall",
+    ["Major Second"] = "Große Sekunde",
+    ["Major Seventh"] = "Große Septime",
+    ["Major Sixth"] = "Große Sexte",
+    ["Major Third"] = "Große Terz",
+    ["Minor Second"] = "Kleine Sekunde",
+    ["Minor Seventh"] = "Kleine Septime",
+    ["Minor Sixth"] = "Kleine Sexte",
+    ["Minor Third"] = "Kleine Terz",
+    ["Perfect Fifth"] = "Reine Quinte",
+    ["Perfect Fourth"] = "Reine Quarte",
+    ["Perfect Octave"] = "Reine Oktave",
+    ["Perfect Unison"] = "Reine Prime",
+    ["Pitch"] = "Tonhöhe",
+    ["Plus Octaves"] = "Plus Oktaven",
+    ["Preserve Existing Notes"] = "Bestehende Noten beibehalten",
+    ["Simplify Spelling"] = "Notation vereinfachen",
+    ["Transposition Error"] = "Transpositionsfehler",
+    ["Up"] = "Hoch",
+    ["OK"] = "OK",
+    ["Cancel"] = "Abbrechen",
+}
+
+return loc
diff --git a/src/localization/transpose_chromatic_es.lua b/src/localization/transpose_chromatic_es.lua
index f1cc196b..925371bc 100644
--- a/src/localization/transpose_chromatic_es.lua
+++ b/src/localization/transpose_chromatic_es.lua
@@ -3,9 +3,42 @@
 ]]
 local loc = {
     ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
-        "Finale no puede representar algunas de las notas traspuestas. Estas notas no se han cambiado.",
-    ["Number Of Steps"] = "Número De Pasos",
+       "Finale no puede representar algunas de las notas traspuestas. Estas notas no se han cambiado.",
+    ["Augmented Fifth"] = "Quinta aumentada",
+    ["Augmented Fourth"] = "Cuarta aumentada",
+    ["Augmented Second"] = "Segunda aumentada",
+    ["Augmented Seventh"] = "Séptima aumentada",
+    ["Augmented Sixth"] = "Sexta aumentada",
+    ["Augmented Third"] = "Tercera aumentada",
+    ["Augmented Unison"] = "Unísono aumentado",
+    ["Diminished Fifth"] = "Quinta disminuida",
+    ["Diminished Fourth"] = "Cuarta disminuida",
+    ["Diminished Octave"] = "Octava disminuida",
+    ["Diminished Second"] = "Segunda disminuida",
+    ["Diminished Seventh"] = "Séptima disminuida",
+    ["Diminished Sixth"] = "Sexta disminuida",
+    ["Diminished Third"] = "Tercera disminuida",
+    ["Direction"] = "Dirección",
+    ["Down"] = "Abajo",
+    ["Interval"] = "Intervalo",
+    ["Major Second"] = "Segunda mayor",
+    ["Major Seventh"] = "Séptima mayor",
+    ["Major Sixth"] = "Sexta mayor",
+    ["Major Third"] = "Tercera mayor",
+    ["Minor Second"] = "Segunda menor",
+    ["Minor Seventh"] = "Séptima menor",
+    ["Minor Sixth"] = "Sexta menor",
+    ["Minor Third"] = "Tercera menor",
+    ["Perfect Fifth"] = "Quinta justa",
+    ["Perfect Fourth"] = "Cuarta justa",
+    ["Perfect Octave"] = "Octava justa",
+    ["Perfect Unison"] = "Unísono justo",
+    ["Pitch"] = "Tono",
+    ["Plus Octaves"] = "Más Octavas",
+    ["Preserve Existing Notes"] = "Preservar notas existentes",
+    ["Simplify Spelling"] = "Simplificar enarmonización",
     ["Transposition Error"] = "Error de trasposición",
+    ["Up"] = "Arriba",
     ["OK"] = "Aceptar",
     ["Cancel"] = "Cancelar",
 }
diff --git a/src/mixin/FCMControl.lua b/src/mixin/FCMControl.lua
index 2884619f..e39d7900 100644
--- a/src/mixin/FCMControl.lua
+++ b/src/mixin/FCMControl.lua
@@ -12,7 +12,6 @@ $module FCMControl
 ]] --
 local mixin = require("library.mixin")
 local mixin_helper = require("library.mixin_helper")
-
 local localization = require("library.localization")
 
 local class = {Methods = {}}
@@ -408,7 +407,7 @@ Removes a handler added with `AddHandleCommand`.
 @ key (string) The key into the localization table. If there is no entry in the appropriate localization table, the key is the text.
 ]]
 function methods:SetLocalizedText(key)
-    mixin_helper.assert_argument_type(2, key, "string", "FCString")
+    mixin_helper.assert_argument_type(2, key, "string")
 
     self:SetText(localization.localize(key))
 end
diff --git a/src/mixin/FCMCtrlPopup.lua b/src/mixin/FCMCtrlPopup.lua
index fb9bac43..96ffa276 100644
--- a/src/mixin/FCMCtrlPopup.lua
+++ b/src/mixin/FCMCtrlPopup.lua
@@ -14,6 +14,7 @@ $module FCMCtrlPopup
 local mixin = require("library.mixin")
 local mixin_helper = require("library.mixin_helper")
 local utils = require("library.utils")
+local localization = require("library.localization")
 
 local class = {Methods = {}}
 local methods = class.Methods
@@ -263,6 +264,26 @@ function methods:AddStrings(...)
     end
 end
 
+
+--[[
+% AddLocalizedStrings
+
+**[Fluid]**
+
+Adds multiple localized strings to the popup.
+
+@ self (FCMCtrlPopup)
+@ ... (string) keys of strings to be added. If no localization is found, the key is added.
+]]
+function methods:AddLocalizedStrings(...)
+    for i = 1, select("#", ...) do
+        local v = select(i, ...)
+        mixin_helper.assert_argument_type(i + 1, v, "string")
+
+        mixin.FCMCtrlPopup.AddString(self, localization.localize(v))
+    end
+end
+
 --[[
 % GetStrings
 
diff --git a/src/mixin/FCMUI.lua b/src/mixin/FCMUI.lua
index a738d1ca..f64c0df7 100644
--- a/src/mixin/FCMUI.lua
+++ b/src/mixin/FCMUI.lua
@@ -8,7 +8,6 @@ $module FCMUI
 ]] --
 local mixin = require("library.mixin") -- luacheck: ignore
 local mixin_helper = require("library.mixin_helper")
-
 local localization = require("library.localization")
 
 local class = {Methods = {}}
@@ -56,8 +55,8 @@ Displays a localized error message.
 @ title_key (string) The key into the localization table. If there is no entry in the appropriate localization table, the key is the title.
 ]]
 function methods:AlertLocalizedError(message_key, title_key)
-    mixin_helper.assert_argument_type(2, message_key, "string", "FCString")
-    mixin_helper.assert_argument_type(3, title_key, "string", "FCString")
+    mixin_helper.assert_argument_type(2, message_key, "string")
+    mixin_helper.assert_argument_type(3, title_key, "string")
 
     self:AlertError(localization.localize(message_key), localization.localize(title_key))
 end
diff --git a/src/transpose_chromatic.lua b/src/transpose_chromatic.lua
index 66b8c702..afb2543c 100644
--- a/src/transpose_chromatic.lua
+++ b/src/transpose_chromatic.lua
@@ -1,4 +1,4 @@
-function plugindef()
+function plugindef(locale)
     finaleplugin.RequireSelection = false
     finaleplugin.HandlesUndo = true -- not recognized by JW Lua or RGP Lua v0.55
     finaleplugin.Author = "Robert Patterson"
@@ -29,54 +29,25 @@ function plugindef()
         Later versions of RGP Lua (0.58 or higher) ignore this configuration file (if it exists) and read the correct
         information from the Finale document.
     ]]
-    return "Transpose Chromatic...", "Transpose Chromatic", "Chromatic transposition of selected region (supports microtone systems)."
-end
-
--- luacheck: ignore 11./global_dialog
-
-if not finenv.RetainLuaState then
-    -- do initial setup once per Lua state
-    interval_names = {
-        "Perfect Unison",
-        "Augmented Unison",
-        "Diminished Second",
-        "Minor Second",
-        "Major Second",
-        "Augmented Second",
-        "Diminished Third",
-        "Minor Third",
-        "Major Third",
-        "Augmented Third",
-        "Diminished Fourth",
-        "Perfect Fourth",
-        "Augmented Fourth",
-        "Diminished Fifth",
-        "Perfect Fifth",
-        "Augmented Fifth",
-        "Diminished Sixth",
-        "Minor Sixth",
-        "Major Sixth",
-        "Augmented Sixth",
-        "Diminished Seventh",
-        "Minor Seventh",
-        "Major Seventh",
-        "Augmented Seventh",
-        "Diminished Octave",
-        "Perfect Octave"
+    local loc = {}
+    loc.en = {
+        menu = "Transpose Chromatic",
+        desc = "Chromatic transposition of selected region (supports microtone systems)."
     }
-
-    interval_disp_alts = {
-        {0,0},  {0,1},                      -- unisons
-        {1,-2}, {1,-1}, {1,0}, {1,1},       -- 2nds
-        {2,-2}, {2,-1}, {2,0}, {2,1},       -- 3rds
-        {3,-1}, {3,0},  {3,1},              -- 4ths
-        {4,-1}, {4,0},  {4,1},              -- 5ths
-        {5,-2}, {5,-1}, {5,0}, {5,1},       -- 6ths
-        {6,-2}, {6,-1}, {6,0}, {6,1},       -- 7ths
-        {7,-1}, {7,0}                       -- octaves
+    loc.es = {
+        menu = "Trasponer cromático",
+        desc = "Trasposición cromática de la región seleccionada (soporta sistemas de microtono)."
     }
+    loc.de = {
+        menu = "Transponieren chromatisch",
+        desc = "Chromatische Transposition des ausgewählten Abschnittes (unterstützt Mikrotonsysteme)."
+    }
+    local t = locale and loc[locale:sub(1,2)] or loc.en
+    return t.menu .. "...", t.menu, t.desc
 end
 
+-- luacheck: ignore 11./global_dialog
+
 if not finenv.IsRGPLua then
     local path = finale.FCString()
     path:SetRunningLuaFolderPath()
@@ -85,6 +56,48 @@ end
 
 local transposition = require("library.transposition")
 local mixin = require("library.mixin")
+local loc = require("library.localization")
+local utils = require("library.utils")
+
+interval_names = interval_names or {
+    "Perfect Unison",
+    "Augmented Unison",
+    "Diminished Second",
+    "Minor Second",
+    "Major Second",
+    "Augmented Second",
+    "Diminished Third",
+    "Minor Third",
+    "Major Third",
+    "Augmented Third",
+    "Diminished Fourth",
+    "Perfect Fourth",
+    "Augmented Fourth",
+    "Diminished Fifth",
+    "Perfect Fifth",
+    "Augmented Fifth",
+    "Diminished Sixth",
+    "Minor Sixth",
+    "Major Sixth",
+    "Augmented Sixth",
+    "Diminished Seventh",
+    "Minor Seventh",
+    "Major Seventh",
+    "Augmented Seventh",
+    "Diminished Octave",
+    "Perfect Octave"
+}
+
+interval_disp_alts = interval_disp_alts or {
+    {0,0},  {0,1},                      -- unisons
+    {1,-2}, {1,-1}, {1,0}, {1,1},       -- 2nds
+    {2,-2}, {2,-1}, {2,0}, {2,1},       -- 3rds
+    {3,-1}, {3,0},  {3,1},              -- 4ths
+    {4,-1}, {4,0},  {4,1},              -- 5ths
+    {5,-2}, {5,-1}, {5,0}, {5,1},       -- 6ths
+    {6,-2}, {6,-1}, {6,0}, {6,1},       -- 7ths
+    {7,-1}, {7,0}                       -- octaves
+}
 
 function do_transpose_chromatic(direction, interval_index, simplify, plus_octaves, preserve_originals)
     if finenv.Region():IsEmpty() then
@@ -93,7 +106,7 @@ function do_transpose_chromatic(direction, interval_index, simplify, plus_octave
     local interval = direction * interval_disp_alts[interval_index][1]
     local alteration = direction * interval_disp_alts[interval_index][2]
     plus_octaves = direction * plus_octaves
-    local undostr = "Transpose Chromatic " .. tostring(finenv.Region().StartMeasure)
+    local undostr = ({plugindef(loc.get_locale())})[2] .. " " .. tostring(finenv.Region().StartMeasure)
     if finenv.Region().StartMeasure ~= finenv.Region().EndMeasure then
         undostr = undostr .. " - " .. tostring(finenv.Region().EndMeasure)
     end
@@ -111,37 +124,77 @@ function do_transpose_chromatic(direction, interval_index, simplify, plus_octave
         finenv.StartNewUndoBlock(undostr, true) -- JW Lua automatically terminates the final undo block we start here
     end
     if not success then
-        finenv.UI():AlertError("Finale is unable to represent some of the transposed pitches. These pitches were left at their original value.", "Transposition Error")
+        global_dialog:CreateUI():AlertLocalizedError(
+            "Finale is unable to represent some of the transposed pitches. These pitches were left unchanged.",
+            "Transposition Error"
+        )
     end
     return success
 end
 
 function create_dialog_box()
-    local dialog = mixin.FCXCustomLuaWindow():SetTitle("Transpose Chromatic")
+    local dialog = mixin.FCXCustomLuaWindow()
+        :SetTitle(plugindef(loc.get_locale()):gsub("%.%.%.", ""))
     local current_y = 0
     local y_increment = 26
     local x_increment = 85
     -- direction
-    dialog:CreateStatic(0, current_y + 2):SetText("Direction:")
-    dialog:CreatePopup(x_increment, current_y, "direction_choice"):AddStrings("Up", "Down"):SetWidth(x_increment):SetSelectedItem(0)
+    dialog:CreateStatic(0, current_y + 2, "direction_label")
+        :SetLocalizedText("Direction")
+        :SetWidth(x_increment - 5)
+        :_FallbackCall("DoAutoResizeWidth", nil, true)
+    dialog:CreatePopup(x_increment, current_y, "direction_choice")
+        :AddLocalizedStrings("Up", "Down"):SetWidth(x_increment)
+        :SetSelectedItem(0)
+        :_FallbackCall("DoAutoResizeWidth", nil, true)
+        :_FallbackCall("AssureNoHorizontalOverlap", nil, dialog:GetControl("direction_label"), 5)
     current_y = current_y + y_increment
     -- interval
-    dialog:CreateStatic(0, current_y + 2):SetText("Interval:")
-    dialog:CreatePopup(x_increment, current_y, "interval_choice"):AddStrings(table.unpack(interval_names)):SetWidth(140):SetSelectedItem(0)
+    dialog:CreateStatic(0, current_y + 2, "interval_label")
+        :SetLocalizedText("Interval")
+        :SetWidth(x_increment - 5)
+        :_FallbackCall("DoAutoResizeWidth", nil, true)
+    dialog:CreatePopup(x_increment, current_y, "interval_choice")
+        :AddLocalizedStrings(table.unpack(interval_names))
+        :SetWidth(140)
+        :SetSelectedItem(0)
+        :_FallbackCall("DoAutoResizeWidth", nil, true)
+        :_FallbackCall("AssureNoHorizontalOverlap", nil, dialog:GetControl("interval_label"), 5)
+        :_FallbackCall("HorizontallyAlignLeftWith", nil, dialog:GetControl("direction_choice"))
     current_y = current_y + y_increment
     -- simplify checkbox
-    dialog:CreateCheckbox(0, current_y + 2, "do_simplify"):SetText("Simplify Spelling"):SetWidth(140):SetCheck(0)
+    dialog:CreateCheckbox(0, current_y + 2, "do_simplify")
+        :SetLocalizedText("Simplify Spelling")
+        :SetWidth(140)
+        :SetCheck(0)
+        :_FallbackCall("DoAutoResizeWidth", nil, true)
     current_y = current_y + y_increment
     -- plus octaves
-    dialog:CreateStatic(0, current_y + 2):SetText("Plus Octaves:")
-    local edit_x = x_increment + (finenv.UI():IsOnMac() and 4 or 0)
-    dialog:CreateEdit(edit_x, current_y, "plus_octaves"):SetText("")
+    dialog:CreateStatic(0, current_y + 2, "plus_octaves_label")
+        :SetLocalizedText("Plus Octaves")
+        :SetWidth(x_increment - 5)
+        :_FallbackCall("DoAutoResizeWidth", nil, true)
+    local edit_offset_x = utils.win_mac(0, 4)
+    dialog:CreateEdit(x_increment + edit_offset_x, current_y, "plus_octaves")
+        :SetText("")
+        :_FallbackCall("AssureNoHorizontalOverlap", nil, dialog:GetControl("plus_octaves_label"), 5)
+        :_FallbackCall("HorizontallyAlignLeftWith", nil, dialog:GetControl("direction_choice"), edit_offset_x)
     current_y = current_y + y_increment
     -- preserve existing notes
-    dialog:CreateCheckbox(0, current_y + 2, "do_preserve"):SetText("Preserve Existing Notes"):SetWidth(140):SetCheck(0)
+    dialog:CreateCheckbox(0, current_y + 2, "do_preserve")
+        :SetLocalizedText("Preserve Existing Notes")
+        :SetWidth(140)
+        :SetCheck(0)
+        :_FallbackCall("DoAutoResizeWidth", nil, true)
+    current_y = current_y + y_increment -- luacheck: ignore
     -- OK/Cxl
     dialog:CreateOkButton()
+        :SetLocalizedText("OK")
+        :_FallbackCall("DoAutoResizeWidth", nil, true)
     dialog:CreateCancelButton()
+        :SetLocalizedText("Cancel")
+        :_FallbackCall("DoAutoResizeWidth", nil, true)
+    -- registrations
     dialog:RegisterHandleOkButtonPressed(function(self)
             local direction = 1 -- up
             if self:GetControl("direction_choice"):GetSelectedItem() > 0 then

From 42492247d4f9f776698fa53b8f74b21d0007894d Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Thu, 1 Feb 2024 11:33:09 -0600
Subject: [PATCH 32/61] enharmonic transposing

---
 .../transpose_enharmonic_down_de.lua           | 10 ++++++++++
 .../transpose_enharmonic_down_es.lua           | 10 ++++++++++
 .../transpose_enharmonic_up_de.lua             | 10 ++++++++++
 .../transpose_enharmonic_up_es.lua             | 10 ++++++++++
 src/transpose_enharmonic_down.lua              | 18 ------------------
 src/transpose_enharmonic_up.lua                | 18 ------------------
 6 files changed, 40 insertions(+), 36 deletions(-)
 create mode 100644 src/localization/transpose_enharmonic_down_de.lua
 create mode 100644 src/localization/transpose_enharmonic_down_es.lua
 create mode 100644 src/localization/transpose_enharmonic_up_de.lua
 create mode 100644 src/localization/transpose_enharmonic_up_es.lua

diff --git a/src/localization/transpose_enharmonic_down_de.lua b/src/localization/transpose_enharmonic_down_de.lua
new file mode 100644
index 00000000..5df9e0c7
--- /dev/null
+++ b/src/localization/transpose_enharmonic_down_de.lua
@@ -0,0 +1,10 @@
+--[[
+    German localization for transpose_by_step.lua
+]]
+local loc = {
+    ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
+        "Finale kann einige der transponierten Tönhöhen nicht darstellen. Diese Tönhöhen wurden unverändert gelassen.",
+    ["Transposition Error"] = "Transpositionsfehler"
+}
+
+return loc
diff --git a/src/localization/transpose_enharmonic_down_es.lua b/src/localization/transpose_enharmonic_down_es.lua
new file mode 100644
index 00000000..847fec99
--- /dev/null
+++ b/src/localization/transpose_enharmonic_down_es.lua
@@ -0,0 +1,10 @@
+--[[
+    Spanish localization for transpose_by_step.lua
+]]
+local loc = {
+    ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
+        "Finale no puede representar algunas de las notas traspuestas. Estas notas no se han cambiado.",
+    ["Transposition Error"] = "Error de trasposición"
+}
+
+return loc
diff --git a/src/localization/transpose_enharmonic_up_de.lua b/src/localization/transpose_enharmonic_up_de.lua
new file mode 100644
index 00000000..5df9e0c7
--- /dev/null
+++ b/src/localization/transpose_enharmonic_up_de.lua
@@ -0,0 +1,10 @@
+--[[
+    German localization for transpose_by_step.lua
+]]
+local loc = {
+    ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
+        "Finale kann einige der transponierten Tönhöhen nicht darstellen. Diese Tönhöhen wurden unverändert gelassen.",
+    ["Transposition Error"] = "Transpositionsfehler"
+}
+
+return loc
diff --git a/src/localization/transpose_enharmonic_up_es.lua b/src/localization/transpose_enharmonic_up_es.lua
new file mode 100644
index 00000000..847fec99
--- /dev/null
+++ b/src/localization/transpose_enharmonic_up_es.lua
@@ -0,0 +1,10 @@
+--[[
+    Spanish localization for transpose_by_step.lua
+]]
+local loc = {
+    ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
+        "Finale no puede representar algunas de las notas traspuestas. Estas notas no se han cambiado.",
+    ["Transposition Error"] = "Error de trasposición"
+}
+
+return loc
diff --git a/src/transpose_enharmonic_down.lua b/src/transpose_enharmonic_down.lua
index c2442091..34fd86c0 100644
--- a/src/transpose_enharmonic_down.lua
+++ b/src/transpose_enharmonic_down.lua
@@ -46,24 +46,6 @@ end
 local transposition = require("library.transposition")
 local loc = require('library.localization')
 
-loc.en = loc.en or {
-    ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
-        "Finale is unable to represent some of the transposed pitches. These pitches were left unchanged.",
-    ["Transposition Error"] = "Transposition Error",
-}
-
-loc.es = loc.es or {
-    ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
-        "Finale no puede representar algunas de las notas traspuestas. Estas notas no se han cambiado.",
-    ["Transposition Error"] = "Error de trasposición",
-}
-
-loc.de = loc.de or {
-    ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
-        "Finale kann einige der transponierten Tönhöhen nicht darstellen. Diese Tönhöhen wurden unverändert gelassen.",
-    ["Transposition Error"] = "Transpositionsfehler",
-}
-
 function transpose_enharmonic_down()
     local success = true
     for entry in eachentrysaved(finenv.Region()) do
diff --git a/src/transpose_enharmonic_up.lua b/src/transpose_enharmonic_up.lua
index dcc5e452..857dadab 100644
--- a/src/transpose_enharmonic_up.lua
+++ b/src/transpose_enharmonic_up.lua
@@ -46,24 +46,6 @@ end
 local transposition = require("library.transposition")
 local loc = require('library.localization')
 
-loc.en = loc.en or {
-    ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
-        "Finale is unable to represent some of the transposed pitches. These pitches were left unchanged.",
-    ["Transposition Error"] = "Transposition Error",
-}
-
-loc.es = loc.es or {
-    ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
-        "Finale no puede representar algunas de las notas traspuestas. Estas notas no se han cambiado.",
-    ["Transposition Error"] = "Error de trasposición",
-}
-
-loc.de = loc.de or {
-    ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
-        "Finale kann einige der transponierten Tönhöhen nicht darstellen. Diese Tönhöhen wurden unverändert gelassen.",
-    ["Transposition Error"] = "Transpositionsfehler",
-}
-
 function transpose_enharmonic_up()
     local success = true
     for entry in eachentrysaved(finenv.Region()) do

From e13bf80e2d4cfc6f415499bd1bf1c49ead77c794 Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Thu, 1 Feb 2024 11:35:26 -0600
Subject: [PATCH 33/61] lint errors

---
 samples/auto_layout.lua | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/samples/auto_layout.lua b/samples/auto_layout.lua
index 22e87a87..1b9b068c 100644
--- a/samples/auto_layout.lua
+++ b/samples/auto_layout.lua
@@ -187,7 +187,6 @@ function create_dialog()
     local dlg = mixin.FCXCustomLuaWindow()
     dlg:SetTitle(localization.localize("Test Autolayout With Localisation"))
 
-    local y = 0
     local line_no = 0
     local y_increment = 22
     local label_edit_separ = 3
@@ -314,7 +313,7 @@ function create_dialog()
         end
     end
     ctrl_cbobox:SetSelectedItem(0)
-    line_no = line_no + 1
+    line_no = line_no + 1 -- luacheck: ignore
 
     line_no = start_line_no
     local ctrl_radiobuttons = dlg:CreateRadioButtonGroup(0, line_no * y_increment, 3)

From b1d8df19c83113c231ef1d45ef34de18f144a32c Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Fri, 2 Feb 2024 08:50:20 -0600
Subject: [PATCH 34/61] refactor folder layout for localization

---
 src/library/localization.lua                  | 58 +++++++++++++------
 .../de.lua}                                   |  0
 .../es.lua}                                   |  0
 .../de.lua}                                   |  0
 .../es.lua}                                   |  0
 .../de.lua}                                   |  0
 .../es.lua}                                   |  0
 .../de.lua}                                   |  0
 .../es.lua}                                   |  0
 src/mixin/FCMControl.lua                      |  4 +-
 src/mixin/FCMCtrlPopup.lua                    |  4 +-
 src/mixin/FCMUI.lua                           |  4 +-
 src/transpose_by_step.lua                     |  8 +--
 src/transpose_chromatic.lua                   | 20 +++----
 14 files changed, 59 insertions(+), 39 deletions(-)
 rename src/localization/{transpose_by_step_de.lua => transpose_by_step/de.lua} (100%)
 rename src/localization/{transpose_by_step_es.lua => transpose_by_step/es.lua} (100%)
 rename src/localization/{transpose_chromatic_de.lua => transpose_chromatic/de.lua} (100%)
 rename src/localization/{transpose_chromatic_es.lua => transpose_chromatic/es.lua} (100%)
 rename src/localization/{transpose_enharmonic_down_de.lua => transpose_enharmonic_down/de.lua} (100%)
 rename src/localization/{transpose_enharmonic_down_es.lua => transpose_enharmonic_down/es.lua} (100%)
 rename src/localization/{transpose_enharmonic_up_de.lua => transpose_enharmonic_up/de.lua} (100%)
 rename src/localization/{transpose_enharmonic_up_es.lua => transpose_enharmonic_up/es.lua} (100%)

diff --git a/src/library/localization.lua b/src/library/localization.lua
index ef1d534a..8b3352ce 100644
--- a/src/library/localization.lua
+++ b/src/library/localization.lua
@@ -4,6 +4,15 @@ $module Localization
 This library provides localization services to scripts. Note that this library cannot be used inside
 a `plugindef` function, because the Lua plugin does not load any dependencies when it calls `plugindef`.
 
+**Executive Summary**
+
+- Create language tables containing each user-facing string as key with a translation as the value.
+- Save them in the `localization` subdirectory as shown below.
+- Use the `...Localized` methods with `mixin` or if not using `mixin`, require the `localization`
+library directly and wrap any user-facing string in a call to `localization.localize`.
+
+**Details**
+
 To use the library, scripts must define each localization
 as a table appended to this library table. If you provide region-specific localizations, you should also
 provide a generic localization for the 2-character language code as a fallback.
@@ -13,12 +22,6 @@ local localization = require("library.localization")
 --
 -- append localizations to the table returned by `require`:
 --
-localization.en = localization.en or {
-    ["Hello"] = "Hello",
-    ["Goodbye"] = "Goodbye",
-    ["Computer"] = "Computer"
-}
-
 localization.es = localization.es or {
     ["Hello"] = "Hola",
     ["Goodbye"] = "Adiós",
@@ -38,21 +41,40 @@ localization.jp = localization.jp or {
 }
 ```
 
-The keys do not have to be in English, but they should be the same in all tables. You can embed the localizations
-in your script or include them with `require`. Example:
+The keys do not have to be in English, but they should be the same in all tables. For scripts in the `src` directory of
+the FinalaLua GitHub repository, it is recommended to place localization tables in the `localization` subdirectory as
+follows.
 
 ```
-local region_code = "de_CH" -- get this from `finenv.UI():GetUserLocaleName(): you could also strip out just the language code "de"
-local localization_table_name = "localization_" region_code
-localization[region_code] = require(localization_table_name)
+src/
+    my_highly_useful_script.lua
+    localization/
+        my_highly_useful_script/
+            de.lua
+            es.lua
+            es_ES.lua
+            jp.lua
+            ...
+
 ```
 
-In this case, `localization_de_CH.lua` could be installed in the folder alongside the localized script. This is just
-one possible approach. You can manage the dependencies in the manner that is best for your script. The easiest
-deployment will always be to avoid dependencies and embed the localizations in your script.
+Note that it is not necessary to provide a table for the language the keys are in. That is,
+if the keys are in English, it is not necessary to provide `en.lua`. Each of the localization `lua` files should return
+a table as described above.
+
+If you wish to add another language, you simply add it to the subfolder for the script, and no further action is
+required.
+
+The `mixin` library provides automatic localization with the `...Localized` methods. Localized versions of text-based
+`mixin` methods should be added as needed, if they do not already exist. If your script does not require the
+`mixin` library, then you can require the `localization` library in your script and call `localization.localize`
+directly.
 
-The `library.localization_developer` library provides tools for automatically generating localization tables to
-copy into scripts. You can then edit them to suit your needs.
+Due to the architecture of the Lua environment on Finale, it is not possible to use this library to localize strings
+in the `plugindef` function. Those must be handled directly inside the script. However, if you call the `plugindef`
+function inside your script, it is recommended to pass `localization.get_locale()` to the `plugindef` function. This
+guarantees that the plugindef function will return strings that are the closest match to the locale the library
+is running with.
 ]]
 
 local localization = {}
@@ -102,13 +124,11 @@ local function get_localized_table(try_locale)
     if type(localization[try_locale]) == "table" then
         return localization[try_locale]
     end
-    local require_library = "localization" .. "." .. script_name .. "_" .. try_locale
+    local require_library = "localization" .. "." .. script_name .. "." .. try_locale
     local success, result = pcall(function() return require(require_library) end)
     if success and type(result) == "table" then
         localization[try_locale] = result
     else
-        print("unable to require " .. require_library)
-        print(result)
         -- doing this allows us to only try to require it once
         localization[try_locale] = {}
     end
diff --git a/src/localization/transpose_by_step_de.lua b/src/localization/transpose_by_step/de.lua
similarity index 100%
rename from src/localization/transpose_by_step_de.lua
rename to src/localization/transpose_by_step/de.lua
diff --git a/src/localization/transpose_by_step_es.lua b/src/localization/transpose_by_step/es.lua
similarity index 100%
rename from src/localization/transpose_by_step_es.lua
rename to src/localization/transpose_by_step/es.lua
diff --git a/src/localization/transpose_chromatic_de.lua b/src/localization/transpose_chromatic/de.lua
similarity index 100%
rename from src/localization/transpose_chromatic_de.lua
rename to src/localization/transpose_chromatic/de.lua
diff --git a/src/localization/transpose_chromatic_es.lua b/src/localization/transpose_chromatic/es.lua
similarity index 100%
rename from src/localization/transpose_chromatic_es.lua
rename to src/localization/transpose_chromatic/es.lua
diff --git a/src/localization/transpose_enharmonic_down_de.lua b/src/localization/transpose_enharmonic_down/de.lua
similarity index 100%
rename from src/localization/transpose_enharmonic_down_de.lua
rename to src/localization/transpose_enharmonic_down/de.lua
diff --git a/src/localization/transpose_enharmonic_down_es.lua b/src/localization/transpose_enharmonic_down/es.lua
similarity index 100%
rename from src/localization/transpose_enharmonic_down_es.lua
rename to src/localization/transpose_enharmonic_down/es.lua
diff --git a/src/localization/transpose_enharmonic_up_de.lua b/src/localization/transpose_enharmonic_up/de.lua
similarity index 100%
rename from src/localization/transpose_enharmonic_up_de.lua
rename to src/localization/transpose_enharmonic_up/de.lua
diff --git a/src/localization/transpose_enharmonic_up_es.lua b/src/localization/transpose_enharmonic_up/es.lua
similarity index 100%
rename from src/localization/transpose_enharmonic_up_es.lua
rename to src/localization/transpose_enharmonic_up/es.lua
diff --git a/src/mixin/FCMControl.lua b/src/mixin/FCMControl.lua
index e39d7900..c2e45969 100644
--- a/src/mixin/FCMControl.lua
+++ b/src/mixin/FCMControl.lua
@@ -397,7 +397,7 @@ Removes a handler added with `AddHandleCommand`.
 methods.AddHandleCommand, methods.RemoveHandleCommand = mixin_helper.create_standard_control_event("HandleCommand")
 
 --[[
-% SetLocalizedText
+% SetTextLocalized
 
 **[Fluid]**
 
@@ -406,7 +406,7 @@ Removes a handler added with `AddHandleCommand`.
 @ self (FCMControl)
 @ key (string) The key into the localization table. If there is no entry in the appropriate localization table, the key is the text.
 ]]
-function methods:SetLocalizedText(key)
+function methods:SetTextLocalized(key)
     mixin_helper.assert_argument_type(2, key, "string")
 
     self:SetText(localization.localize(key))
diff --git a/src/mixin/FCMCtrlPopup.lua b/src/mixin/FCMCtrlPopup.lua
index 96ffa276..cc48a120 100644
--- a/src/mixin/FCMCtrlPopup.lua
+++ b/src/mixin/FCMCtrlPopup.lua
@@ -266,7 +266,7 @@ end
 
 
 --[[
-% AddLocalizedStrings
+% AddStringsLocalized
 
 **[Fluid]**
 
@@ -275,7 +275,7 @@ Adds multiple localized strings to the popup.
 @ self (FCMCtrlPopup)
 @ ... (string) keys of strings to be added. If no localization is found, the key is added.
 ]]
-function methods:AddLocalizedStrings(...)
+function methods:AddStringsLocalized(...)
     for i = 1, select("#", ...) do
         local v = select(i, ...)
         mixin_helper.assert_argument_type(i + 1, v, "string")
diff --git a/src/mixin/FCMUI.lua b/src/mixin/FCMUI.lua
index f64c0df7..4e3150c0 100644
--- a/src/mixin/FCMUI.lua
+++ b/src/mixin/FCMUI.lua
@@ -44,7 +44,7 @@ function methods:GetDecimalSeparator(str)
 end
 
 --[[
-% AlertLocalizedError
+% AlertErrorLocalized
 
 **[Fluid]**
 
@@ -54,7 +54,7 @@ Displays a localized error message.
 @ message_key (string) The key into the localization table. If there is no entry in the appropriate localization table, the key is the message.
 @ title_key (string) The key into the localization table. If there is no entry in the appropriate localization table, the key is the title.
 ]]
-function methods:AlertLocalizedError(message_key, title_key)
+function methods:AlertErrorLocalized(message_key, title_key)
     mixin_helper.assert_argument_type(2, message_key, "string")
     mixin_helper.assert_argument_type(3, title_key, "string")
 
diff --git a/src/transpose_by_step.lua b/src/transpose_by_step.lua
index 8b61859b..d3472bfd 100644
--- a/src/transpose_by_step.lua
+++ b/src/transpose_by_step.lua
@@ -82,7 +82,7 @@ function do_transpose_by_step(global_number_of_steps_edit)
         finenv.StartNewUndoBlock(undostr, true) -- JW Lua automatically terminates the final undo block we start here
     end
     if not success then
-        global_dialog:CreateChildUI():AlertLocalizedError(
+        global_dialog:CreateChildUI():AlertErrorLocalized(
             "Finale is unable to represent some of the transposed pitches. These pitches were left unchanged.",
             "Transposition Error"
         )            
@@ -97,7 +97,7 @@ function create_dialog_box()
     local x_increment = 105
     -- number of steps
     dialog:CreateStatic(0, current_y + 2, "steps_label")
-        :SetLocalizedText("Number Of Steps")
+        :SetTextLocalized("Number Of Steps")
         :SetWidth(x_increment - 5)
         :_FallbackCall("DoAutoResizeWidth", nil, true)
     local edit_x = x_increment + utils.win_mac(0, 4)
@@ -106,10 +106,10 @@ function create_dialog_box()
         :_FallbackCall("AssureNoHorizontalOverlap", nil, dialog:GetControl("steps_label"), 5)
     -- ok/cancel
     dialog:CreateOkButton()
-        :SetLocalizedText("OK")
+        :SetTextLocalized("OK")
         :_FallbackCall("DoAutoResizeWidth", nil, true)
     dialog:CreateCancelButton()
-        :SetLocalizedText("Cancel")
+        :SetTextLocalized("Cancel")
         :_FallbackCall("DoAutoResizeWidth", nil, true)
     -- registrations
     dialog:RegisterHandleOkButtonPressed(function(self)
diff --git a/src/transpose_chromatic.lua b/src/transpose_chromatic.lua
index afb2543c..65f01f4f 100644
--- a/src/transpose_chromatic.lua
+++ b/src/transpose_chromatic.lua
@@ -124,7 +124,7 @@ function do_transpose_chromatic(direction, interval_index, simplify, plus_octave
         finenv.StartNewUndoBlock(undostr, true) -- JW Lua automatically terminates the final undo block we start here
     end
     if not success then
-        global_dialog:CreateUI():AlertLocalizedError(
+        global_dialog:CreateUI():AlertErrorLocalized(
             "Finale is unable to represent some of the transposed pitches. These pitches were left unchanged.",
             "Transposition Error"
         )
@@ -140,22 +140,22 @@ function create_dialog_box()
     local x_increment = 85
     -- direction
     dialog:CreateStatic(0, current_y + 2, "direction_label")
-        :SetLocalizedText("Direction")
+        :SetTextLocalized("Direction")
         :SetWidth(x_increment - 5)
         :_FallbackCall("DoAutoResizeWidth", nil, true)
     dialog:CreatePopup(x_increment, current_y, "direction_choice")
-        :AddLocalizedStrings("Up", "Down"):SetWidth(x_increment)
+        :AddStringsLocalized("Up", "Down"):SetWidth(x_increment)
         :SetSelectedItem(0)
         :_FallbackCall("DoAutoResizeWidth", nil, true)
         :_FallbackCall("AssureNoHorizontalOverlap", nil, dialog:GetControl("direction_label"), 5)
     current_y = current_y + y_increment
     -- interval
     dialog:CreateStatic(0, current_y + 2, "interval_label")
-        :SetLocalizedText("Interval")
+        :SetTextLocalized("Interval")
         :SetWidth(x_increment - 5)
         :_FallbackCall("DoAutoResizeWidth", nil, true)
     dialog:CreatePopup(x_increment, current_y, "interval_choice")
-        :AddLocalizedStrings(table.unpack(interval_names))
+        :AddStringsLocalized(table.unpack(interval_names))
         :SetWidth(140)
         :SetSelectedItem(0)
         :_FallbackCall("DoAutoResizeWidth", nil, true)
@@ -164,14 +164,14 @@ function create_dialog_box()
     current_y = current_y + y_increment
     -- simplify checkbox
     dialog:CreateCheckbox(0, current_y + 2, "do_simplify")
-        :SetLocalizedText("Simplify Spelling")
+        :SetTextLocalized("Simplify Spelling")
         :SetWidth(140)
         :SetCheck(0)
         :_FallbackCall("DoAutoResizeWidth", nil, true)
     current_y = current_y + y_increment
     -- plus octaves
     dialog:CreateStatic(0, current_y + 2, "plus_octaves_label")
-        :SetLocalizedText("Plus Octaves")
+        :SetTextLocalized("Plus Octaves")
         :SetWidth(x_increment - 5)
         :_FallbackCall("DoAutoResizeWidth", nil, true)
     local edit_offset_x = utils.win_mac(0, 4)
@@ -182,17 +182,17 @@ function create_dialog_box()
     current_y = current_y + y_increment
     -- preserve existing notes
     dialog:CreateCheckbox(0, current_y + 2, "do_preserve")
-        :SetLocalizedText("Preserve Existing Notes")
+        :SetTextLocalized("Preserve Existing Notes")
         :SetWidth(140)
         :SetCheck(0)
         :_FallbackCall("DoAutoResizeWidth", nil, true)
     current_y = current_y + y_increment -- luacheck: ignore
     -- OK/Cxl
     dialog:CreateOkButton()
-        :SetLocalizedText("OK")
+        :SetTextLocalized("OK")
         :_FallbackCall("DoAutoResizeWidth", nil, true)
     dialog:CreateCancelButton()
-        :SetLocalizedText("Cancel")
+        :SetTextLocalized("Cancel")
         :_FallbackCall("DoAutoResizeWidth", nil, true)
     -- registrations
     dialog:RegisterHandleOkButtonPressed(function(self)

From a9363593758085d899e8b3da226e42f66dce6cb7 Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Fri, 2 Feb 2024 14:21:34 -0600
Subject: [PATCH 35/61] clarify comments

---
 src/library/localization.lua | 91 +++++++++++++++++++++---------------
 1 file changed, 54 insertions(+), 37 deletions(-)

diff --git a/src/library/localization.lua b/src/library/localization.lua
index 8b3352ce..9d8e09a4 100644
--- a/src/library/localization.lua
+++ b/src/library/localization.lua
@@ -13,67 +13,84 @@ library directly and wrap any user-facing string in a call to `localization.loca
 
 **Details**
 
-To use the library, scripts must define each localization
-as a table appended to this library table. If you provide region-specific localizations, you should also
-provide a generic localization for the 2-character language code as a fallback.
+To use the library, scripts must define each localization in a specified subfolder of the `localization` folder.
+If you provide region-specific localizations, you should also provide a generic localization for the 2-character
+language code as a fallback. The directory structure is as follows (where `my_highly_useful_script.lua` is your
+script file).
 
 ```
-local localization = require("library.localization")
---
--- append localizations to the table returned by `require`:
---
-localization.es = localization.es or {
-    ["Hello"] = "Hola",
-    ["Goodbye"] = "Adiós",
-    ["Computer"] = "Computadora"
-}
+src/
+    my_highly_useful_script.lua
+    localization/
+        my_highly_useful_script/
+            de.lua
+            es.lua
+            es_ES.lua
+            jp.lua
+            ...
 
--- specific localization for Spain
--- it is only necessary to specify items that are different than the fallback language table.
-localization.es_ES = localization.es_ES or {
-    ["Computer"] = "Ordenador"
-}
+```
 
-localization.jp = localization.jp or {
+Each localization lua should return a table of keys and translations.
+
+Japanase:
+
+```
+--
+-- jp.lua:
+--
+local t = {
     ["Hello"] = "今日は",
     ["Goodbye"] = "さようなら",
     ["Computer"] =  "コンピュータ" 
 }
+
+return t
 ```
 
-The keys do not have to be in English, but they should be the same in all tables. For scripts in the `src` directory of
-the FinalaLua GitHub repository, it is recommended to place localization tables in the `localization` subdirectory as
-follows.
+Spanish:
 
 ```
-src/
-    my_highly_useful_script.lua
-    localization/
-        my_highly_useful_script/
-            de.lua
-            es.lua
-            es_ES.lua
-            jp.lua
-            ...
+--
+-- es.lua:
+--
+local t = {
+    ["Hello"] = "Hola",
+    ["Goodbye"] = "Adiós",
+    ["Computer"] = "Computadora"
+}
 
+return t
 ```
 
-Note that it is not necessary to provide a table for the language the keys are in. That is,
-if the keys are in English, it is not necessary to provide `en.lua`. Each of the localization `lua` files should return
-a table as described above.
+You can specify vocabulary for a specific locale. It is only necessary to specify items that
+differ from the the fallback language table.
+
+```
+--
+-- es_ES.lua:
+--
+local t = {
+    ["Computer"] = "Ordenador"
+}
+
+return t
+```
 
+The keys do not have to be in English, but they should be the same in all tables. It is not necessary to provide
+a table for the language the keys are in. That is, if the keys are in English, it is not necessary to provide `en.lua`.
 If you wish to add another language, you simply add it to the subfolder for the script, and no further action is
 required.
 
-The `mixin` library provides automatic localization with the `...Localized` methods. Localized versions of text-based
-`mixin` methods should be added as needed, if they do not already exist. If your script does not require the
+The `mixin` library provides automatic localization with the `...Localized` methods. Localized versions of user-facing
+text-based `mixin` methods should be added as needed, if they do not already exist. If your script does not require the
 `mixin` library, then you can require the `localization` library in your script and call `localization.localize`
 directly.
 
 Due to the architecture of the Lua environment on Finale, it is not possible to use this library to localize strings
 in the `plugindef` function. Those must be handled directly inside the script. However, if you call the `plugindef`
 function inside your script, it is recommended to pass `localization.get_locale()` to the `plugindef` function. This
-guarantees that the plugindef function will return strings that are the closest match to the locale the library
+guarantees that the `plugindef` function returns strings that are the closest match to the locale the library
 is running with.
 ]]
 
@@ -138,7 +155,7 @@ end
 --[[
 % localize
 
-Localizes a string based on the localization language
+Localizes a string based on the localization language.
 
 @ input_string (string) the string to be localized
 : (string) the localized version of the string or input_string if not found

From 0f9c83ececf80f46e34fe00c25a3026a176e99e6 Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Sat, 3 Feb 2024 08:29:50 -0600
Subject: [PATCH 36/61] checkpoint localization tool

---
 .luacheckrc                                   |   2 +-
 .../localization_tool.lua                     | 108 ++++++++++++++++--
 2 files changed, 101 insertions(+), 9 deletions(-)
 rename src/library/localization_developer.lua => utilities/localization_tool.lua (59%)

diff --git a/.luacheckrc b/.luacheckrc
index dce7813d..4357c197 100644
--- a/.luacheckrc
+++ b/.luacheckrc
@@ -1,5 +1,5 @@
 -- luacheck: ignore 131
-include_files = { "src/**/*.lua", "samples/**/*.lua"}
+include_files = { "src/**/*.lua", "samples/**/*.lua", "utilities/**/*.lua"}
 exclude_files = {
     "mobdebug.lua",
     "src/lunajson/**/*.lua",
diff --git a/src/library/localization_developer.lua b/utilities/localization_tool.lua
similarity index 59%
rename from src/library/localization_developer.lua
rename to utilities/localization_tool.lua
index aed2ca3d..bc1000e4 100644
--- a/src/library/localization_developer.lua
+++ b/utilities/localization_tool.lua
@@ -1,3 +1,29 @@
+function plugindef()
+    finaleplugin.RequireDocument = false
+    finaleplugin.RequireSelection = false
+    finaleplugin.NoStore = true
+    finaleplugin.ExecuteHttpsCalls = true
+    finaleplugin.Author = "Robert Patterson"
+    finaleplugin.Version = "1.0"
+    finaleplugin.Date = "February 3, 2024"
+    finaleplugin.MinJWLuaVersion = "0.71"
+    finaleplugin.Notes = [[
+        This script provides a set of localization services for developers of scripts to make localization
+        as simple as possible. It uses calls to OpenAI to automatically translate words and phrases. However,
+        such translations should always be checked with fluent speakers before presenting them to users.
+
+        Functions include:
+
+        - Automatically create a table of all quoted strings in the library. The user can then edit this
+            down to the user-facing strings that need to be localized.
+        - Given a table of strings, creates a localization file for a specified language.
+        - Create a localized `plugindef` function for a script.
+        
+        Users of this script will get the best results if they use it in tandem with an indegrated development
+        environment such as Visual Studio Code or with a text editor.
+    ]]
+    return "Localization Tool...", "Localization Tool", "Automates the process of localizing scripts in the Finale Lua repository."
+end
 --[[
 $module Localization for Developers
 
@@ -5,11 +31,15 @@ This library provides a set of localization services for developers of scripts t
 as simple as possible. It uses calls to OpenAI to automatically translate words and phrases.
 ]]
 
-local localization_developer = {}
+-- luacheck: ignore 11./global_dialog
 
 local client = require("library.client")
 local library = require("library.general_library")
 local openai = require("library.openai")
+local mixin = require("library.mixin")
+local utils = require("library.utils")
+
+local tab_str = "    "
 
 --[[
 % create_localized_base_table
@@ -30,7 +60,7 @@ Only the top-level script is searched. This is the script at the path specified
 
 : (table) a table containing the found strings
 ]]
-function localization_developer.create_localized_base_table()
+local function create_localized_base_table()
     local retval = {}
     local file_path = library.calc_script_filepath()
     file_path = client.encode_with_client_codepage(file_path)
@@ -106,23 +136,23 @@ key is always equal to the value. The base table can be in any language.
 @ lang (string) the two-letter language code of the strings in the base table. This is used only to name the table.
 : (string) A string containing a Lua-formatted table of all quoted strings in the script
 ]]
-function localization_developer.create_localized_base_table_string(lang)
-    local t = localization_developer.create_localized_base_table()
+local function create_localized_base_table_string(lang) -- luacheck: ignore
+    local t = create_localized_base_table()
     finenv.UI():TextToClipboard(make_flat_table_string(lang, t))
     finenv.UI():AlertInfo("localization_base table copied to clipboard", "")
 end
 
-function localization_developer.translate_localized_table_string(source_table, source_lang, target_lang)
+local function translate_localized_table_string(source_table, source_lang, target_lang) -- luacheck: ignore
     local table_string = make_flat_table_string(source_lang, source_table)
     local prompt = [[
         I am working on localizing text for a program that prints and plays music. There may be musical
         terminology among the words and phrases that I would like you to translate, as follows.\n
     ]] .. "Here is a lua table of keys and values:\n\n```\n" .. table_string .. "\n```\n" ..
-                        [[
+        [[
                     Provide a string that is Lua source code of a table definition of a table that has the same keys
                     but with the values translated to locale specified by the code
                 ]] .. target_lang .. ". The table name should be `localization." .. target_lang .. "`.\n" ..
-                [[
+        [[
                     Return only the Lua code without any commentary. There may or may not be musical terms
                     in the provided text. This information is provided for context if needed.
                 ]]
@@ -137,4 +167,66 @@ function localization_developer.translate_localized_table_string(source_table, s
     end
 end
 
-return localization_developer
+local function on_generate(_control)
+end
+
+local function on_translate(_control)
+end
+
+local function on_plugindef(_control)
+end
+
+local function create_dialog()
+    local dlg = mixin.FCXCustomLuaWindow()
+        :SetTitle("Localization Helper")
+    local editor_width = 700
+    local editor_height = 300
+    local y_separator = 10
+    local x_separator = 7
+    local button_height = 20
+    --script selection
+    local curr_y = 0
+    dlg:CreatePopup(0, curr_y, "file_list")
+        :SetWidth((2 * editor_width) / 3)
+    curr_y = curr_y + button_height
+    --editor
+    curr_y = curr_y + y_separator
+    local font = finale.FCFontInfo(utils.win_mac("Consolas", "Menlo"), utils.win_mac(9, 11))
+    dlg:CreateTextEditor(0, curr_y, "editor")
+        :SetWidth(editor_width)
+        :SetHeight(editor_height)
+        :SetUseRichText(false)
+        :SetAutomaticEditing(false)
+        :SetFont(font)
+        :SetConvertTabsToSpaces(#tab_str)
+        :SetAutomaticallyIndent(true)
+    curr_y = curr_y + editor_height
+    -- command buttons
+    curr_y = curr_y + y_separator
+    dlg:CreateButton(0, curr_y, "generate")
+        :SetText("Generate Table")
+        :DoAutoResizeWidth()
+        :AddHandleCommand(on_generate)
+    dlg:CreateButton(0, curr_y, "translate")
+        :SetText("Translate Table")
+        :DoAutoResizeWidth()
+        :AssureNoHorizontalOverlap(dlg:GetControl("generate"), x_separator)
+        :AddHandleCommand(on_translate)
+    dlg:CreateButton(0, curr_y, "plugindef")
+        :SetText("Localize Plugindef")
+        :DoAutoResizeWidth()
+        :AssureNoHorizontalOverlap(dlg:GetControl("translate"), x_separator)
+        :AddHandleCommand(on_plugindef)
+    dlg:CreateCloseButton(0, curr_y)
+        :HorizontallyAlignRightWith(dlg:GetControl("editor"))
+    -- registrations
+    -- return
+    return dlg
+end
+
+local function localization_tool()
+    global_dialog = global_dialog or create_dialog()
+    global_dialog:RunModeless()
+end
+
+localization_tool()

From 64d59a80581db4056c756f58c31911ad4683eb39 Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Sat, 3 Feb 2024 09:34:22 -0600
Subject: [PATCH 37/61] checkpoint localization tool

---
 src/localization/transpose_by_step/de.lua     |  6 +-
 src/localization/transpose_by_step/es.lua     |  6 +-
 src/localization/transpose_chromatic/de.lua   |  6 +-
 src/localization/transpose_chromatic/es.lua   |  6 +-
 .../transpose_enharmonic_down/de.lua          |  6 +-
 .../transpose_enharmonic_down/es.lua          |  6 +-
 .../transpose_enharmonic_up/de.lua            |  6 +-
 .../transpose_enharmonic_up/es.lua            |  6 +-
 src/mixin/FCMUI.lua                           | 28 ++++++
 utilities/localization_tool.lua               | 86 +++++++++++++++----
 10 files changed, 119 insertions(+), 43 deletions(-)

diff --git a/src/localization/transpose_by_step/de.lua b/src/localization/transpose_by_step/de.lua
index 804051b9..1346f4c7 100644
--- a/src/localization/transpose_by_step/de.lua
+++ b/src/localization/transpose_by_step/de.lua
@@ -1,6 +1,6 @@
---[[
-    German localization for transpose_by_step.lua
-]]
+--
+-- Localization de.lua for transpose_by_step.lua
+--
 local loc = {
     ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
         "Finale kann einige der transponierten Tönhöhen nicht darstellen. Diese Tönhöhen wurden unverändert gelassen.",
diff --git a/src/localization/transpose_by_step/es.lua b/src/localization/transpose_by_step/es.lua
index f1cc196b..27ffc260 100644
--- a/src/localization/transpose_by_step/es.lua
+++ b/src/localization/transpose_by_step/es.lua
@@ -1,6 +1,6 @@
---[[
-    Spanish localization for transpose_by_step.lua
-]]
+--
+-- Localization es.lua for transpose_by_step.lua
+--
 local loc = {
     ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
         "Finale no puede representar algunas de las notas traspuestas. Estas notas no se han cambiado.",
diff --git a/src/localization/transpose_chromatic/de.lua b/src/localization/transpose_chromatic/de.lua
index d52dcb3b..7b653a36 100644
--- a/src/localization/transpose_chromatic/de.lua
+++ b/src/localization/transpose_chromatic/de.lua
@@ -1,6 +1,6 @@
---[[
-    German localization for transpose_by_step.lua
-]]
+--
+-- Localization de.lua for transpose_chromatic.lua
+--
 local loc = {
     ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
         "Finale kann einige der transponierten Tönhöhen nicht darstellen. Diese Tönhöhen wurden unverändert gelassen.",
diff --git a/src/localization/transpose_chromatic/es.lua b/src/localization/transpose_chromatic/es.lua
index 925371bc..559c64ae 100644
--- a/src/localization/transpose_chromatic/es.lua
+++ b/src/localization/transpose_chromatic/es.lua
@@ -1,6 +1,6 @@
---[[
-    Spanish localization for transpose_by_step.lua
-]]
+--
+-- Localization es.lua for transpose_chromatic.lua
+--
 local loc = {
     ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
        "Finale no puede representar algunas de las notas traspuestas. Estas notas no se han cambiado.",
diff --git a/src/localization/transpose_enharmonic_down/de.lua b/src/localization/transpose_enharmonic_down/de.lua
index 5df9e0c7..37bb90ef 100644
--- a/src/localization/transpose_enharmonic_down/de.lua
+++ b/src/localization/transpose_enharmonic_down/de.lua
@@ -1,6 +1,6 @@
---[[
-    German localization for transpose_by_step.lua
-]]
+--
+-- Localization de.lua for transpose_enharmonic_down.lua
+--
 local loc = {
     ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
         "Finale kann einige der transponierten Tönhöhen nicht darstellen. Diese Tönhöhen wurden unverändert gelassen.",
diff --git a/src/localization/transpose_enharmonic_down/es.lua b/src/localization/transpose_enharmonic_down/es.lua
index 847fec99..6fa8d0e0 100644
--- a/src/localization/transpose_enharmonic_down/es.lua
+++ b/src/localization/transpose_enharmonic_down/es.lua
@@ -1,6 +1,6 @@
---[[
-    Spanish localization for transpose_by_step.lua
-]]
+--
+-- Localization es.lua for transpose_enharmonic_down.lua
+--
 local loc = {
     ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
         "Finale no puede representar algunas de las notas traspuestas. Estas notas no se han cambiado.",
diff --git a/src/localization/transpose_enharmonic_up/de.lua b/src/localization/transpose_enharmonic_up/de.lua
index 5df9e0c7..96378b28 100644
--- a/src/localization/transpose_enharmonic_up/de.lua
+++ b/src/localization/transpose_enharmonic_up/de.lua
@@ -1,6 +1,6 @@
---[[
-    German localization for transpose_by_step.lua
-]]
+--
+-- Localization de.lua for transpose_enharmonic_up.lua
+--
 local loc = {
     ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
         "Finale kann einige der transponierten Tönhöhen nicht darstellen. Diese Tönhöhen wurden unverändert gelassen.",
diff --git a/src/localization/transpose_enharmonic_up/es.lua b/src/localization/transpose_enharmonic_up/es.lua
index 847fec99..364cb9f9 100644
--- a/src/localization/transpose_enharmonic_up/es.lua
+++ b/src/localization/transpose_enharmonic_up/es.lua
@@ -1,6 +1,6 @@
---[[
-    Spanish localization for transpose_by_step.lua
-]]
+--
+-- Localization es.lua for transpose_enharmonic_up.lua
+--
 local loc = {
     ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
         "Finale no puede representar algunas de las notas traspuestas. Estas notas no se han cambiado.",
diff --git a/src/mixin/FCMUI.lua b/src/mixin/FCMUI.lua
index 4e3150c0..31e834b3 100644
--- a/src/mixin/FCMUI.lua
+++ b/src/mixin/FCMUI.lua
@@ -43,6 +43,34 @@ function methods:GetDecimalSeparator(str)
     end
 end
 
+--[[
+% GetUserLocaleName
+
+**[?Fluid] [Override]**
+
+Override Changes:
+- Passing an `FCString` is optional. If omitted, the result is returned as a Lua `string`. If passed, nothing is returned and the method is fluid.
+
+@ self (FCMUI)
+@ [str] (FCString)
+: (string)
+]]
+function methods:GetUserLocaleName(str)
+    mixin_helper.assert_argument_type(2, str, "nil", "FCString")
+
+    local do_return = false
+    if not str then
+        str = temp_str
+        do_return = true
+    end
+
+    self:GetUserLocaleName__(str)
+
+    if do_return then
+        return str.LuaString
+    end
+end
+
 --[[
 % AlertErrorLocalized
 
diff --git a/utilities/localization_tool.lua b/utilities/localization_tool.lua
index bc1000e4..f94000af 100644
--- a/utilities/localization_tool.lua
+++ b/utilities/localization_tool.lua
@@ -40,6 +40,12 @@ local mixin = require("library.mixin")
 local utils = require("library.utils")
 
 local tab_str = "    "
+local src_directory = (function()
+    local curr_path = library.calc_script_filepath()
+    local path_name = finale.FCString()
+    finale.FCString(curr_path):SplitToPathAndFile(path_name, nil)
+    return path_name.LuaString .. "../src/"
+end)()
 
 --[[
 % create_localized_base_table
@@ -60,9 +66,8 @@ Only the top-level script is searched. This is the script at the path specified
 
 : (table) a table containing the found strings
 ]]
-local function create_localized_base_table()
+local function create_localized_base_table(file_path)
     local retval = {}
-    local file_path = library.calc_script_filepath()
     file_path = client.encode_with_client_codepage(file_path)
     local file = io.open(file_path, "r")
     if file then
@@ -104,42 +109,55 @@ local function create_localized_base_table()
                 return nil
             end
         end
-        local file_content = file:read("all")
-        for found_string in extract_strings(file_content) do
-            retval[found_string] = found_string
+        for line in file:lines() do
+            if not string.match(line, "^%s*%-%-") then
+                for found_string in extract_strings(line) do
+                    retval[found_string] = found_string
+                end
+            end
         end
         file:close()
     end
     return retval
 end
 
-local function make_flat_table_string(lang, t)
+local function make_flat_table_string(file_path, lang, t)
+    local file_name = finale.FCString()
+    finale.FCString(file_path):SplitToPathAndFile(nil, file_name)
     local concat = {}
-    table.insert(concat, "localization." .. lang .. " = localization." .. lang .. " or {\n")
+    table.insert(concat, "--\n")
+    table.insert(concat, "-- Localization " .. lang .. ".lua for " .. file_name.LuaString .. "\n")
+    table.insert(concat, "--\n")
+    table.insert(concat, "loc = {\n")
     for k, v in pairsbykeys(t) do
         table.insert(concat, "    [\"" .. tostring(k) .. "\"] = \"" .. tostring(v) .. "\",\n")
     end
-    table.insert(concat, "}\n")
+    table.insert(concat, "}\n\nreturn loc\n")
     return table.concat(concat)
 end
 
+local function set_edit_text(edit_text)
+    global_dialog:GetControl("editor"):SetText(edit_text)
+end
+
 --[[
 % create_localized_base_table_string
 
-Creates and returns a string representing a lua table of localizable strings by searching the top-level script for
-quoted strings. It then copies this string to the clipboard. The primary use case is to be
-a developer tool to aid in the creation of a table to be embedded in the script.
+Creates and displays a string representing a lua table of localizable strings by searching the specified script for
+quoted strings. It then copies this string to the editor. The user can then edit it to include only user-facing
+string and then create translations from that.
 
 The base table is the table that defines the keys for all other languages. For each item in the base table, the
-key is always equal to the value. The base table can be in any language.
+key is always equal to the value. The base table can be in any language. The base table does not need to be saved
+as a localization.
 
-@ lang (string) the two-letter language code of the strings in the base table. This is used only to name the table.
-: (string) A string containing a Lua-formatted table of all quoted strings in the script
+@ file_path (string) the file_path to search for strings.
 ]]
-local function create_localized_base_table_string(lang) -- luacheck: ignore
-    local t = create_localized_base_table()
-    finenv.UI():TextToClipboard(make_flat_table_string(lang, t))
-    finenv.UI():AlertInfo("localization_base table copied to clipboard", "")
+local function create_localized_base_table_string(file_path)
+    local t = create_localized_base_table(file_path)
+    local locale = mixin.UI():GetUserLocaleName()
+    set_edit_text(make_flat_table_string(file_path, locale:sub(1, 2), t))
+    -- finenv.UI():AlertInfo("localization_base table copied to clipboard", "")
 end
 
 local function translate_localized_table_string(source_table, source_lang, target_lang) -- luacheck: ignore
@@ -167,7 +185,32 @@ local function translate_localized_table_string(source_table, source_lang, targe
     end
 end
 
-local function on_generate(_control)
+local on_open -- luacheck: ignore
+local function on_generate(control)
+    local popup = global_dialog:GetControl("file_list")
+    if popup:GetCount() <= 0 then
+        on_open(control)
+    end
+    if popup:GetCount() > 0 then
+        local sel_item = popup:GetSelectedItem()
+        create_localized_base_table_string(popup:GetItemText(sel_item))
+    end
+end
+
+local function on_open(control) -- luacheck: ignore
+    local file_open_dlg = finale.FCFileOpenDialog(global_dialog:CreateChildUI())
+    file_open_dlg:AddFilter(finale.FCString("*.lua"), finale.FCString("Lua source files"))
+    file_open_dlg:SetInitFolder(finale.FCString(src_directory))
+    file_open_dlg:SetWindowTitle(finale.FCString("Open Lua Source File"))
+    if file_open_dlg:Execute() then
+        local fc_name = finale.FCString()
+        file_open_dlg:GetFileName(fc_name)
+        local popup = global_dialog:GetControl("file_list")
+        -- ToDo: search for and select if it already exists 
+        popup:AddString(fc_name.LuaString)
+        popup:SetSelectedItem(popup:GetCount() - 1)
+        on_generate(control)
+    end
 end
 
 local function on_translate(_control)
@@ -203,9 +246,14 @@ local function create_dialog()
     curr_y = curr_y + editor_height
     -- command buttons
     curr_y = curr_y + y_separator
+    dlg:CreateButton(0, curr_y, "open")
+        :SetText("Open Script")
+        :DoAutoResizeWidth()
+        :AddHandleCommand(on_open)
     dlg:CreateButton(0, curr_y, "generate")
         :SetText("Generate Table")
         :DoAutoResizeWidth()
+        :AssureNoHorizontalOverlap(dlg:GetControl("open"), x_separator)
         :AddHandleCommand(on_generate)
     dlg:CreateButton(0, curr_y, "translate")
         :SetText("Translate Table")

From f8ef97bdfd45c4628e1242f5aee027f7ef952526 Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Sat, 3 Feb 2024 11:58:55 -0600
Subject: [PATCH 38/61] localization tool semi-working. main things missing are
 plugindef support and async openai calls

---
 utilities/localization_tool.lua | 146 ++++++++++++++++++++++++++------
 1 file changed, 122 insertions(+), 24 deletions(-)

diff --git a/utilities/localization_tool.lua b/utilities/localization_tool.lua
index f94000af..7fd83f75 100644
--- a/utilities/localization_tool.lua
+++ b/utilities/localization_tool.lua
@@ -24,12 +24,6 @@ function plugindef()
     ]]
     return "Localization Tool...", "Localization Tool", "Automates the process of localizing scripts in the Finale Lua repository."
 end
---[[
-$module Localization for Developers
-
-This library provides a set of localization services for developers of scripts to make localization
-as simple as possible. It uses calls to OpenAI to automatically translate words and phrases.
-]]
 
 -- luacheck: ignore 11./global_dialog
 
@@ -47,6 +41,23 @@ local src_directory = (function()
     return path_name.LuaString .. "../src/"
 end)()
 
+global_contents = global_contents or {}
+local in_popup_handler = false
+local popup_cur_sel = -1
+local in_text_change_event = false
+
+local finale_supported_languages = {
+    ["Dutch"] = "nl",
+    ["English"] = "en",
+    ["German"] = "de",
+    ["French"] = "fr",
+    ["Italian"] = "it",
+    ["Japanese"] = "ja",
+    ["Polish"] = "pl",
+    ["Spanish"] = "es",
+    ["Swedish"] = "sv"
+}
+
 --[[
 % create_localized_base_table
 
@@ -140,6 +151,15 @@ local function set_edit_text(edit_text)
     global_dialog:GetControl("editor"):SetText(edit_text)
 end
 
+local function get_sel_text()
+    local popup = global_dialog:GetControl("file_list")
+    local sel_item = popup:GetSelectedItem()
+    if sel_item >= 0 and sel_item < popup:GetCount() then
+        return popup:GetItemText(popup:GetSelectedItem())
+    end
+    return nil
+end
+
 --[[
 % create_localized_base_table_string
 
@@ -156,36 +176,86 @@ as a localization.
 local function create_localized_base_table_string(file_path)
     local t = create_localized_base_table(file_path)
     local locale = mixin.UI():GetUserLocaleName()
-    set_edit_text(make_flat_table_string(file_path, locale:sub(1, 2), t))
+    local table_text = make_flat_table_string(file_path, locale:sub(1, 2), t)
+    global_contents[file_path] = table_text
+    set_edit_text(table_text)
     -- finenv.UI():AlertInfo("localization_base table copied to clipboard", "")
 end
 
-local function translate_localized_table_string(source_table, source_lang, target_lang) -- luacheck: ignore
-    local table_string = make_flat_table_string(source_lang, source_table)
+local function set_enable_all(state)
+    global_dialog:GetControl("file_list"):SetEnable(state)    
+    global_dialog:GetControl("editor"):SetEnable(state)    
+    global_dialog:GetControl("open"):SetEnable(state)    
+    global_dialog:GetControl("generate"):SetEnable(state)    
+    global_dialog:GetControl("translate"):SetEnable(state)    
+    global_dialog:GetControl("plugindef"):SetEnable(state)    
+end
+
+local function translate_localized_table_string(table_string, target_lang)
     local prompt = [[
         I am working on localizing text for a program that prints and plays music. There may be musical
         terminology among the words and phrases that I would like you to translate, as follows.\n
-    ]] .. "Here is a lua table of keys and values:\n\n```\n" .. table_string .. "\n```\n" ..
-        [[
-                    Provide a string that is Lua source code of a table definition of a table that has the same keys
-                    but with the values translated to locale specified by the code
-                ]] .. target_lang .. ". The table name should be `localization." .. target_lang .. "`.\n" ..
+    ]] .. "Here is Lua source code for a table of keys and values:\n\n```\n" .. table_string .. "\n```\n" ..
         [[
-                    Return only the Lua code without any commentary. There may or may not be musical terms
-                    in the provided text. This information is provided for context if needed.
-                ]]
-
+                    Provide a string that is Lua source code of a similar table definition that has the same keys
+                    but with values that are translations of the keys for the locale specified by the code
+                ]] .. target_lang .. [[. Return only the Lua code without any commentary. The output source code should
+                    be identical to the input (including comments) except the values should be translated and any
+                    locale code in the comment should be changed to match ]] .. target_lang .. "." ..
+                    [[
+                        There may or may not be musical terms in the provided text.
+                        This information is provided for context if needed.
+                    ]]
+    print(prompt)
+    set_enable_all(false)
+    -- ToDo: async call here
     local success, result = openai.create_completion("gpt-4", prompt, 0.2, 30)
+    set_enable_all(true)
     if success then
         local retval = string.gsub(result.choices[1].message.content, "```", "")
         finenv.UI():TextToClipboard(retval)
-        finenv.UI():AlertInfo("localization." .. target_lang .. " table copied to clipboard", "")
+        finenv.UI():AlertInfo("localization for " .. target_lang .. " table copied to clipboard", "")
     else
         finenv.UI():AlertError(result, "OpenAI Error")
     end
 end
 
-local on_open -- luacheck: ignore
+local function on_text_change(control)
+    assert(type(control) == "userdata" and control.ClassName, "argument 1 expected FCCtrlPopup, got " .. type(control))
+    assert(control:ClassName() == "FCCtrlTextEditor", "argument 1 expected FCCtrlTextEditor, got " .. control:ClassName())
+    if in_text_change_event then
+        return
+    end
+    in_text_change_event = true
+    local sel_text = get_sel_text()
+    if sel_text then
+        global_contents[sel_text] = control:GetText()
+    end
+    in_text_change_event = false
+end
+
+local function on_popup(control)
+    assert(type(control) == "userdata" and control.ClassName, "argument 1 expected FCCtrlPopup, got " .. type(control))
+    assert(control:ClassName() == "FCCtrlPopup", "argument 1 expected FCCtrlPopup, got " .. control:ClassName())
+    if in_popup_handler then
+        return
+    end
+    in_popup_handler = true
+    local selected_item = control:GetSelectedItem()
+    if popup_cur_sel ~= selected_item then -- avoid Windows churn
+        popup_cur_sel = selected_item
+        control:SetEnable(false)
+        local sel_text = control:GetItemText(selected_item)
+        local sel_content = global_contents[sel_text] or ""
+        set_edit_text(sel_content)
+        control:SetEnable(true)
+        popup_cur_sel = control:GetSelectedItem()
+    end
+    in_popup_handler = false
+    -- do not put edit_text in focus here, because it messes up Windows
+end
+
+local on_open
 local function on_generate(control)
     local popup = global_dialog:GetControl("file_list")
     if popup:GetCount() <= 0 then
@@ -195,9 +265,10 @@ local function on_generate(control)
         local sel_item = popup:GetSelectedItem()
         create_localized_base_table_string(popup:GetItemText(sel_item))
     end
+    global_dialog:GetControl("editor"):SetKeyboardFocus()
 end
 
-local function on_open(control) -- luacheck: ignore
+on_open = function(control)
     local file_open_dlg = finale.FCFileOpenDialog(global_dialog:CreateChildUI())
     file_open_dlg:AddFilter(finale.FCString("*.lua"), finale.FCString("Lua source files"))
     file_open_dlg:SetInitFolder(finale.FCString(src_directory))
@@ -206,17 +277,34 @@ local function on_open(control) -- luacheck: ignore
         local fc_name = finale.FCString()
         file_open_dlg:GetFileName(fc_name)
         local popup = global_dialog:GetControl("file_list")
-        -- ToDo: search for and select if it already exists 
+        if global_contents[fc_name.LuaString] then
+            for x = 0, popup:GetCount() - 1 do
+                local x_text = popup:GetItemText(x)
+                if x_text == fc_name.LuaString then
+                    popup:SetSelectedItem(x)
+                    on_popup(popup)
+                    return
+                end
+            end
+        end
         popup:AddString(fc_name.LuaString)
         popup:SetSelectedItem(popup:GetCount() - 1)
         on_generate(control)
     end
+    global_dialog:GetControl("editor"):SetKeyboardFocus()
 end
 
 local function on_translate(_control)
+    local sel_text = get_sel_text() or ""
+    local content = global_contents[sel_text] or ""
+    local lang_text = global_dialog:GetControl("lang_list"):GetText()
+    lang_text = finale_supported_languages[lang_text] or lang_text
+    translate_localized_table_string(content, lang_text) -- ToDo: ask for language code somehow
+    global_dialog:GetControl("editor"):SetKeyboardFocus()
 end
 
 local function on_plugindef(_control)
+    global_dialog:GetControl("editor"):SetKeyboardFocus()
 end
 
 local function create_dialog()
@@ -231,18 +319,28 @@ local function create_dialog()
     local curr_y = 0
     dlg:CreatePopup(0, curr_y, "file_list")
         :SetWidth((2 * editor_width) / 3)
+        :AddHandleCommand(on_popup)
+    local lang_list = dlg:CreateComboBox(0, curr_y, "lang_list")
+        :DoAutoResizeWidth()
+    for lang, _ in pairsbykeys(finale_supported_languages) do
+        lang_list:AddString(finale.FCString(lang))
+    end
+    lang_list:SetText("Spanish")
     curr_y = curr_y + button_height
     --editor
     curr_y = curr_y + y_separator
     local font = finale.FCFontInfo(utils.win_mac("Consolas", "Menlo"), utils.win_mac(9, 11))
-    dlg:CreateTextEditor(0, curr_y, "editor")
+    local editor = dlg:CreateTextEditor(0, curr_y, "editor")
         :SetWidth(editor_width)
         :SetHeight(editor_height)
         :SetUseRichText(false)
         :SetAutomaticEditing(false)
+        :SetWordWrap(false)
         :SetFont(font)
         :SetConvertTabsToSpaces(#tab_str)
         :SetAutomaticallyIndent(true)
+        :AddHandleCommand(on_text_change)
+    lang_list:HorizontallyAlignRightWith(editor, utils.win_mac(0, -3))
     curr_y = curr_y + editor_height
     -- command buttons
     curr_y = curr_y + y_separator
@@ -266,7 +364,7 @@ local function create_dialog()
         :AssureNoHorizontalOverlap(dlg:GetControl("translate"), x_separator)
         :AddHandleCommand(on_plugindef)
     dlg:CreateCloseButton(0, curr_y)
-        :HorizontallyAlignRightWith(dlg:GetControl("editor"))
+        :HorizontallyAlignRightWith(editor)
     -- registrations
     -- return
     return dlg

From 12f0a652ea742b476e8aacf1778c363a8bbf0f7d Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Sat, 3 Feb 2024 14:19:55 -0600
Subject: [PATCH 39/61] refactor sync openai calls to async

---
 utilities/localization_tool.lua | 58 +++++++++++++++++++++++----------
 1 file changed, 40 insertions(+), 18 deletions(-)

diff --git a/utilities/localization_tool.lua b/utilities/localization_tool.lua
index 7fd83f75..4bea55c3 100644
--- a/utilities/localization_tool.lua
+++ b/utilities/localization_tool.lua
@@ -20,7 +20,8 @@ function plugindef()
         - Create a localized `plugindef` function for a script.
         
         Users of this script will get the best results if they use it in tandem with an indegrated development
-        environment such as Visual Studio Code or with a text editor.
+        environment (IDE) such as Visual Studio Code or with a text editor. The script copies text to the clipboard
+        which you can then paste into the IDE or editor.
     ]]
     return "Localization Tool...", "Localization Tool", "Automates the process of localizing scripts in the Finale Lua repository."
 end
@@ -33,6 +34,9 @@ local openai = require("library.openai")
 local mixin = require("library.mixin")
 local utils = require("library.utils")
 
+local osutils = require("luaosutils")
+local https = osutils.internet
+
 local tab_str = "    "
 local src_directory = (function()
     local curr_path = library.calc_script_filepath()
@@ -45,6 +49,7 @@ global_contents = global_contents or {}
 local in_popup_handler = false
 local popup_cur_sel = -1
 local in_text_change_event = false
+local https_session
 
 local finale_supported_languages = {
     ["Dutch"] = "nl",
@@ -182,16 +187,32 @@ local function create_localized_base_table_string(file_path)
     -- finenv.UI():AlertInfo("localization_base table copied to clipboard", "")
 end
 
-local function set_enable_all(state)
+local function set_enable_all()
+    local state = (https_session == nil) -- disable (send false) if https_session is not nil
     global_dialog:GetControl("file_list"):SetEnable(state)    
+    global_dialog:GetControl("lang_list"):SetEnable(state)    
     global_dialog:GetControl("editor"):SetEnable(state)    
     global_dialog:GetControl("open"):SetEnable(state)    
     global_dialog:GetControl("generate"):SetEnable(state)    
     global_dialog:GetControl("translate"):SetEnable(state)    
-    global_dialog:GetControl("plugindef"):SetEnable(state)    
+    global_dialog:GetControl("plugindef"):SetEnable(state)
+    -- The Close button is deliberately left enabled at all times
 end
 
 local function translate_localized_table_string(table_string, target_lang)
+    local function callback(success, result)
+        https_session = nil
+        set_enable_all()
+        if success then
+            local retval = string.gsub(result.choices[1].message.content, "```", "")
+            retval = retval:gsub("^%s+", "")            -- remove leading whitespace
+            retval = retval:gsub("%s+$", "") .. "\n"    -- remove trailing whitespace and add line ending
+            mixin.UI():TextToClipboard(retval)
+            mixin.UI():AlertInfo("localization for " .. target_lang .. " table copied to clipboard", "")
+        else
+            mixin.UI():AlertError(result, "OpenAI Error")
+        end
+    end
     local prompt = [[
         I am working on localizing text for a program that prints and plays music. There may be musical
         terminology among the words and phrases that I would like you to translate, as follows.\n
@@ -207,17 +228,8 @@ local function translate_localized_table_string(table_string, target_lang)
                         This information is provided for context if needed.
                     ]]
     print(prompt)
-    set_enable_all(false)
-    -- ToDo: async call here
-    local success, result = openai.create_completion("gpt-4", prompt, 0.2, 30)
-    set_enable_all(true)
-    if success then
-        local retval = string.gsub(result.choices[1].message.content, "```", "")
-        finenv.UI():TextToClipboard(retval)
-        finenv.UI():AlertInfo("localization for " .. target_lang .. " table copied to clipboard", "")
-    else
-        finenv.UI():AlertError(result, "OpenAI Error")
-    end
+    https_session = openai.create_completion("gpt-4", prompt, 0.2, callback)
+    set_enable_all()
 end
 
 local function on_text_change(control)
@@ -255,11 +267,11 @@ local function on_popup(control)
     -- do not put edit_text in focus here, because it messes up Windows
 end
 
-local on_open
+local on_script_open
 local function on_generate(control)
     local popup = global_dialog:GetControl("file_list")
     if popup:GetCount() <= 0 then
-        on_open(control)
+        on_script_open(control)
     end
     if popup:GetCount() > 0 then
         local sel_item = popup:GetSelectedItem()
@@ -268,7 +280,7 @@ local function on_generate(control)
     global_dialog:GetControl("editor"):SetKeyboardFocus()
 end
 
-on_open = function(control)
+on_script_open = function(control)
     local file_open_dlg = finale.FCFileOpenDialog(global_dialog:CreateChildUI())
     file_open_dlg:AddFilter(finale.FCString("*.lua"), finale.FCString("Lua source files"))
     file_open_dlg:SetInitFolder(finale.FCString(src_directory))
@@ -299,6 +311,10 @@ local function on_translate(_control)
     local content = global_contents[sel_text] or ""
     local lang_text = global_dialog:GetControl("lang_list"):GetText()
     lang_text = finale_supported_languages[lang_text] or lang_text
+    if not lang_text:match("^[a-z][a-z]$") and not lang_text:match("^[a-z][a-z]_[A-Z][A-Z]$") then
+        mixin.UI():AlertError(lang_text .. " is not a valid language or locale code.", "Invalid Entry")
+        return
+    end
     translate_localized_table_string(content, lang_text) -- ToDo: ask for language code somehow
     global_dialog:GetControl("editor"):SetKeyboardFocus()
 end
@@ -307,6 +323,11 @@ local function on_plugindef(_control)
     global_dialog:GetControl("editor"):SetKeyboardFocus()
 end
 
+local function on_close()
+    https_session = https.cancel_session(https_session)
+    set_enable_all()
+end
+
 local function create_dialog()
     local dlg = mixin.FCXCustomLuaWindow()
         :SetTitle("Localization Helper")
@@ -347,7 +368,7 @@ local function create_dialog()
     dlg:CreateButton(0, curr_y, "open")
         :SetText("Open Script")
         :DoAutoResizeWidth()
-        :AddHandleCommand(on_open)
+        :AddHandleCommand(on_script_open)
     dlg:CreateButton(0, curr_y, "generate")
         :SetText("Generate Table")
         :DoAutoResizeWidth()
@@ -366,6 +387,7 @@ local function create_dialog()
     dlg:CreateCloseButton(0, curr_y)
         :HorizontallyAlignRightWith(editor)
     -- registrations
+    dlg:RegisterCloseWindow(on_close)
     -- return
     return dlg
 end

From 8c854295b1ffde29ca42357962b990abb3113767 Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Sat, 3 Feb 2024 14:22:33 -0600
Subject: [PATCH 40/61] line ending

---
 utilities/localization_tool.lua | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/utilities/localization_tool.lua b/utilities/localization_tool.lua
index 4bea55c3..de5913ad 100644
--- a/utilities/localization_tool.lua
+++ b/utilities/localization_tool.lua
@@ -321,7 +321,7 @@ end
 
 local function on_plugindef(_control)
     global_dialog:GetControl("editor"):SetKeyboardFocus()
-end
+end 
 
 local function on_close()
     https_session = https.cancel_session(https_session)

From 88953d9da650b10981ffab6000995d392e250cd3 Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Sun, 4 Feb 2024 10:10:02 -0600
Subject: [PATCH 41/61] plugindef localization kind of working

---
 utilities/localization_tool.lua | 170 ++++++++++++++++++++++++++++++--
 1 file changed, 160 insertions(+), 10 deletions(-)

diff --git a/utilities/localization_tool.lua b/utilities/localization_tool.lua
index de5913ad..76dcdb76 100644
--- a/utilities/localization_tool.lua
+++ b/utilities/localization_tool.lua
@@ -85,7 +85,7 @@ Only the top-level script is searched. This is the script at the path specified
 local function create_localized_base_table(file_path)
     local retval = {}
     file_path = client.encode_with_client_codepage(file_path)
-    local file = io.open(file_path, "r")
+    local file <close> = io.open(file_path, "r")
     if file then
         local function extract_strings(file_content)
             local i = 1
@@ -132,7 +132,6 @@ local function create_localized_base_table(file_path)
                 end
             end
         end
-        file:close()
     end
     return retval
 end
@@ -146,7 +145,7 @@ local function make_flat_table_string(file_path, lang, t)
     table.insert(concat, "--\n")
     table.insert(concat, "loc = {\n")
     for k, v in pairsbykeys(t) do
-        table.insert(concat, "    [\"" .. tostring(k) .. "\"] = \"" .. tostring(v) .. "\",\n")
+        table.insert(concat, tab_str .. "[\"" .. tostring(k) .. "\"] = \"" .. tostring(v) .. "\",\n")
     end
     table.insert(concat, "}\n\nreturn loc\n")
     return table.concat(concat)
@@ -187,6 +186,129 @@ local function create_localized_base_table_string(file_path)
     -- finenv.UI():AlertInfo("localization_base table copied to clipboard", "")
 end
 
+--[[
+% extract_plugindef
+
+Extracts the plugindef function from the input script file_path.
+@ file_path (string) the file_path of the script to search for a plugindef function
+: (table) the lines of the plugindef function in a table of strings
+: (boolean) locale already exists
+]]
+local function extract_plugindef(file_path)
+    local retval = {}
+    local locale_exists = false
+    file_path = client.encode_with_client_codepage(file_path)
+    local file <close> = io.open(file_path, "r")
+    if file then
+        local found_first = false
+        for line in file:lines() do
+            if line:find("function plugindef") == 1 then
+                found_first = true
+                locale_exists = line:match("plugindef%s*%(%s*locale%s*%)")
+            end
+            if found_first then
+                table.insert(retval, line)
+            end
+            if line:find("end") == 1 then
+                break
+            end
+        end
+    end
+    return retval, locale_exists
+end
+
+--[[
+% extract_plugindef_locale_table
+
+Extracts the existing user-facing strings from a plugindef function into a string that contains
+Lua code for a locale table. This can be inserted into a new plugindef function or sent to OpenAI
+to be translated. It also modifies the plugindef lines to pull from the table.
+
+For best results, certain conventions must be followed:
+
+- The `plugindef` function and its `end` statment should have no whitespace at the beginning of the line.
+- Additional menu options, undo strings, and descriptions should be entirely on separate lines from their
+double-bracket delimiters.
+- The return strings should be on a single line and use double-quotes.
+
+If if you follow these conventions, you will likely have to edit the result somewhat.
+
+@ table A table consisting of strings that are the lines of the plugindef function. This value is also modified
+to pull the strings from a locale table `t`
+: string A string containing Lua code that defines a table of keys and values
+]]
+local function extract_plugindef_locale_table(plugindef_function)
+    local concat = {}
+    table.insert(concat, "{\n")
+    local index = 1
+    while (plugindef_function[index]) do
+        local line = plugindef_function[index]
+        local function check_additional_strings(property, key)
+            local pattern = "%s*finaleplugin%." .. property .. "%s*="
+            if line:match("^" .. pattern .. "%s*%[%[") then
+                plugindef_function[index] = line:gsub("^(" .. pattern .. ").-$", "%1" .. " t." .. key .. "\n")
+                table.insert(concat, tab_str)
+                table.insert(concat, tab_str)
+                table.insert(concat, key)
+                table.insert(concat, " = [[\n")
+                while (plugindef_function[index + 1]) do
+                    local next_line = plugindef_function[index + 1]
+                    table.insert(concat, tab_str)
+                    table.insert(concat, next_line)
+                    table.insert(concat, "\n")
+                    table.remove(plugindef_function, index + 1)
+                    if next_line:find("]]") then
+                        table.insert(concat, ",\n")
+                        break
+                    else
+                        table.insert(concat, "\n")
+                    end
+                end
+                return true
+            end
+            return false
+        end
+        if check_additional_strings("AdditionalMenuOptions", "addl_menus") then         -- luacheck: ignore
+        elseif check_additional_strings("AdditionalUndoText", "addl_undos") then        -- luacheck: ignore
+        elseif check_additional_strings("AdditionalDescriptions", "addl_descs") then    -- luacheck: ignore
+        elseif line:match("^%s*return") then
+            local new_return = line:gsub("^(%s*return).-$", "%1" .. " ")
+            local got_menu, got_undo, got_desc
+            for match, _ in line:gmatch('("([^"]*)")') do
+                local function insert_retval(key, value)
+                    table.insert(concat, tab_str)
+                    table.insert(concat, tab_str)
+                    table.insert(concat, key)
+                    table.insert(concat, " = ")
+                    table.insert(concat, value)
+                    table.insert(concat, ",\n")
+                end
+                if not got_menu then
+                    insert_retval("menu", match)
+                    new_return = new_return .. " t.menu"
+                    got_menu = true
+                elseif not got_undo then
+                    insert_retval("undo", match)
+                    new_return = new_return .. ", t.undo"
+                    got_undo = true
+                elseif not got_desc then
+                    insert_retval("desc", match)
+                    new_return = new_return .. ", t.desc"
+                    got_desc = true
+                else
+                    break
+                end
+            end
+            plugindef_function[index] = new_return .. "\n"
+        else
+            plugindef_function[index] = plugindef_function[index] .. "\n"
+        end
+        index = index + 1
+    end
+    table.insert(concat, tab_str .. "}\n")
+    return table.concat(concat)
+end
+
 local function set_enable_all()
     local state = (https_session == nil) -- disable (send false) if https_session is not nil
     global_dialog:GetControl("file_list"):SetEnable(state)    
@@ -227,7 +349,6 @@ local function translate_localized_table_string(table_string, target_lang)
                         There may or may not be musical terms in the provided text.
                         This information is provided for context if needed.
                     ]]
-    print(prompt)
     https_session = openai.create_completion("gpt-4", prompt, 0.2, callback)
     set_enable_all()
 end
@@ -320,6 +441,30 @@ local function on_translate(_control)
 end
 
 local function on_plugindef(_control)
+    local sel_text = get_sel_text()
+    local text_copied = false
+    if sel_text then
+        local plugindef_function, locale_exists = extract_plugindef(sel_text)
+        if #plugindef_function > 0 then
+            local base_strings = extract_plugindef_locale_table(plugindef_function)
+            if #base_strings > 0 then
+                if not locale_exists then
+                    plugindef_function[1] = "function plugindef(locale)\n"
+                end
+                local locale = mixin.UI():GetUserLocaleName()
+                table.insert(plugindef_function, 2, tab_str .. "local loc = {}\n")
+                table.insert(plugindef_function, 3, tab_str .. "loc." .. locale:sub(1, 2) .. " = " .. base_strings)
+                table.insert(plugindef_function, 4,
+                    tab_str .. "local t = locale and loc[locale:sub(1, 2)] or loc." .. locale:sub(1, 2) .. "\n")
+            end
+            mixin.UI():TextToClipboard(table.concat(plugindef_function))
+            mixin.UI():AlertInfo("Localized plugindef function copied to clipboard.", "")
+            text_copied = true
+        end
+    end
+    if not text_copied then
+        mixin.UI():AlertError("No plugindef function found.", "")
+    end
     global_dialog:GetControl("editor"):SetKeyboardFocus()
 end 
 
@@ -338,15 +483,25 @@ local function create_dialog()
     local button_height = 20
     --script selection
     local curr_y = 0
+    dlg:CreateButton(0, curr_y, "open")
+        :SetText("Open...")
+        :DoAutoResizeWidth()
+        :AddHandleCommand(on_script_open)
     dlg:CreatePopup(0, curr_y, "file_list")
         :SetWidth((2 * editor_width) / 3)
+        :AssureNoHorizontalOverlap(dlg:GetControl("open"), x_separator)
         :AddHandleCommand(on_popup)
     local lang_list = dlg:CreateComboBox(0, curr_y, "lang_list")
+        :SetWidth(0)
         :DoAutoResizeWidth()
+    local lang_index = 0
     for lang, _ in pairsbykeys(finale_supported_languages) do
         lang_list:AddString(finale.FCString(lang))
+        if lang == "Spanish" then
+            lang_list:SetSelectedItem(lang_index)
+        end
+        lang_index = lang_index + 1
     end
-    lang_list:SetText("Spanish")
     curr_y = curr_y + button_height
     --editor
     curr_y = curr_y + y_separator
@@ -365,14 +520,9 @@ local function create_dialog()
     curr_y = curr_y + editor_height
     -- command buttons
     curr_y = curr_y + y_separator
-    dlg:CreateButton(0, curr_y, "open")
-        :SetText("Open Script")
-        :DoAutoResizeWidth()
-        :AddHandleCommand(on_script_open)
     dlg:CreateButton(0, curr_y, "generate")
         :SetText("Generate Table")
         :DoAutoResizeWidth()
-        :AssureNoHorizontalOverlap(dlg:GetControl("open"), x_separator)
         :AddHandleCommand(on_generate)
     dlg:CreateButton(0, curr_y, "translate")
         :SetText("Translate Table")

From 4704eb2936f5623ed2ec71a7452364ca531bf724 Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Sun, 4 Feb 2024 12:18:58 -0600
Subject: [PATCH 42/61] plugindef localization is working

---
 utilities/localization_tool.lua | 17 +++++++----------
 1 file changed, 7 insertions(+), 10 deletions(-)

diff --git a/utilities/localization_tool.lua b/utilities/localization_tool.lua
index 76dcdb76..f6adf237 100644
--- a/utilities/localization_tool.lua
+++ b/utilities/localization_tool.lua
@@ -246,7 +246,7 @@ local function extract_plugindef_locale_table(plugindef_function)
         local function check_additional_strings(property, key)
             local pattern = "%s*finaleplugin%." .. property .. "%s*="
             if line:match("^" .. pattern .. "%s*%[%[") then
-                plugindef_function[index] = line:gsub("^(" .. pattern .. ").-$", "%1" .. " t." .. key .. "\n")
+                plugindef_function[index] = line:gsub("^(" .. pattern .. ").-$", "%1" .. " t." .. key)
                 table.insert(concat, tab_str)
                 table.insert(concat, tab_str)
                 table.insert(concat, key)
@@ -255,7 +255,6 @@ local function extract_plugindef_locale_table(plugindef_function)
                     local next_line = plugindef_function[index + 1]
                     table.insert(concat, tab_str)
                     table.insert(concat, next_line)
-                    table.insert(concat, "\n")
                     table.remove(plugindef_function, index + 1)
                     if next_line:find("]]") then
                         table.insert(concat, ",\n")
@@ -299,13 +298,11 @@ local function extract_plugindef_locale_table(plugindef_function)
                     break
                 end
             end
-            plugindef_function[index] = new_return .. "\n"
-        else
-            plugindef_function[index] = plugindef_function[index] .. "\n"
+            plugindef_function[index] = new_return 
         end
         index = index + 1
     end
-    table.insert(concat, tab_str .. "}\n")
+    table.insert(concat, tab_str .. "}")
     return table.concat(concat)
 end
 
@@ -449,15 +446,15 @@ local function on_plugindef(_control)
             local base_strings = extract_plugindef_locale_table(plugindef_function)
             if #base_strings > 0 then
                 if not locale_exists then
-                    plugindef_function[1] = "function plugindef(locale)\n"
+                    plugindef_function[1] = "function plugindef(locale)"
                 end
                 local locale = mixin.UI():GetUserLocaleName()
-                table.insert(plugindef_function, 2, tab_str .. "local loc = {}\n")
+                table.insert(plugindef_function, 2, tab_str .. "local loc = {}")
                 table.insert(plugindef_function, 3, tab_str .. "loc." .. locale:sub(1, 2) .. " = " .. base_strings)
                 table.insert(plugindef_function, 4,
-                    tab_str .. "local t = locale and loc[locale:sub(1, 2)] or loc." .. locale:sub(1, 2) .. "\n")
+                    tab_str .. "local t = locale and loc[locale:sub(1, 2)] or loc." .. locale:sub(1, 2))
             end
-            mixin.UI():TextToClipboard(table.concat(plugindef_function))
+            mixin.UI():TextToClipboard(table.concat(plugindef_function, "\n") .. "\n")
             mixin.UI():AlertInfo("Localized plugindef function copied to clipboard.", "")
             text_copied = true
         end

From 2efafc14f04e89830fab5dccbb7258134df8e79f Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Sun, 4 Feb 2024 17:40:01 -0600
Subject: [PATCH 43/61] localize an example with a lot of additional menu items

---
 src/baseline_move_reset.lua | 116 +++++++++++++++++++++++++-----------
 1 file changed, 80 insertions(+), 36 deletions(-)

diff --git a/src/baseline_move_reset.lua b/src/baseline_move_reset.lua
index f7e28806..118eb6a4 100644
--- a/src/baseline_move_reset.lua
+++ b/src/baseline_move_reset.lua
@@ -1,9 +1,83 @@
-function plugindef()
+function plugindef(locale)
+    local loc = {}
+    loc.en = {
+        addl_menus = [[
+            Move Lyric Baselines Up
+            Reset Lyric Baselines
+            Move Expression Baseline Above Down
+            Move Expression Baseline Above Up
+            Reset Expression Baseline Above
+            Move Expression Baseline Below Down
+            Move Expression Baseline Below Up
+            Reset Expression Baseline Below
+            Move Chord Baseline Down
+            Move Chord Baseline Up
+            Reset Chord Baseline
+            Move Fretboard Baseline Down
+            Move Fretboard Baseline Up
+            Reset Fretboard Baseline
+        ]],
+        addl_descs = [[
+            Moves all lyrics baselines up one space in the selected systems
+            Resets all lyrics baselines to their defaults in the selected systems
+            Moves the expression above baseline down one space in the selected systems
+            Moves the expression above baseline up one space in the selected systems
+            Resets the expression above baselines in the selected systems
+            Moves the expression below baseline down one space in the selected systems
+            Moves the expression below baseline up one space in the selected systems
+            Resets the expression below baselines in the selected systems
+            Moves the chord baseline down one space in the selected systems
+            Moves the chord baseline up one space in the selected systems
+            Resets the chord baselines in the selected systems
+            Moves the fretboard baseline down one space in the selected systems
+            Moves the fretboard baseline up one space in the selected systems
+            Resets the fretboard baselines in the selected systems
+        ]],
+        menu = "Move Lyric Baselines Down",
+        desc = "Moves all lyrics baselines down one space in the selected systems",
+    }
+    loc.es = {
+        addl_menus = [[
+            Mover las líneas de referencia de las letras hacia arriba
+            Restablecer las líneas de referencia de las letras
+            Mover la línea de referencia por encima de las expresiones hacia abajo
+            Mover la línea de referencia por encima de las expresiones hacia arriba
+            Restablecer la línea de referencia por encima de las expresiones
+            Mover la línea de referencia por abajo de las expresiones hacia abajo
+            Mover la línea de referencia por abajo de las expresiones hacia arriba
+            Restablecer la línea de referencia por abajo de las expresiones
+            Mover la línea de referencia de los acordes hacia abajo
+            Mover la línea de referencia de los acordes hacia arriba
+            Restablecer la línea de referencia de los acordes
+            Mover la línea de referencia de los trastes hacia abajo
+            Mover la línea de referencia de los trastes hacia arriba
+            Restablecer la línea de referencia de los trastes
+        ]],
+        addl_descs = [[
+            Mueve todas las líneas de referencia de las letras un espacio hacia arriba en los sistemas de pentagramas seleccionadas
+            Restablece todas las líneas de referencia de las letras a su valor predeterminado en los sistemas de pentagramas seleccionadas
+            Mueve la línea de referencia por encima de las expresiones hacia abajo un espacio en los sistemas de pentagramas seleccionadas
+            Mueve la línea de referencia por encima de las expresiones hacia arriba un espacio en los sistemas de pentagramas seleccionadas
+            Restablece la línea de referencia por encima de las expresiones superior en los sistemas de pentagramas seleccionadas
+            Mueve la línea de referencia por abajo de las expresiones hacia abajo un espacio en los sistemas de pentagramas seleccionadas
+            Mueve la línea de referencia por abajo de las expresiones hacia arriba un espacio en los sistemas de pentagramas seleccionadas
+            Restablece la línea de referencia por abajo de las expresiones inferior en los sistemas de pentagramas seleccionadas
+            Mueve la línea de referencia de los acordes hacia abajo un espacio en los sistemas de pentagramas seleccionadas
+            Mueve la línea de referencia de los acordes hacia arriba un espacio en los sistemas de pentagramas seleccionadas
+            Restablece las líneas de referencia de los acordes en los sistemas de pentagramas seleccionadas
+            Mueve la línea de referencia de los trastes hacia abajo un espacio en los sistemas de pentagramas seleccionadas
+            Mueve la línea de referencia de los trastes hacia arriba un espacio en los sistemas de pentagramas seleccionadas
+            Restablece las líneas de referencia de los trastes en los sistemas de pentagramas seleccionadas
+        ]],
+        menu = "Mover las líneas de referencia de las letras hacia abajo",
+        desc = "Mueve todas las líneas de referencia de las letras un espacio hacia abajo en los sistemas de pentagramas seleccionadas",
+    }
+    local t = locale and loc[locale:sub(1, 2)] or loc.en
     finaleplugin.RequireSelection = true
     finaleplugin.Author = "Robert Patterson"
-    finaleplugin.Version = "1.0"
+    finaleplugin.Version = "1.1"
     finaleplugin.Copyright = "CC0 https://creativecommons.org/publicdomain/zero/1.0/"
-    finaleplugin.Date = "May 15, 2022"
+    finaleplugin.Date = "February 4, 2024"
     finaleplugin.CategoryTags = "Baseline"
     finaleplugin.AuthorURL = "http://robertgpatterson.com"
     finaleplugin.MinJWLuaVersion = 0.62
@@ -29,38 +103,8 @@ function plugindef()
 
         A value in a prefix overrides any setting in a configuration file.
     ]]
-    finaleplugin.AdditionalMenuOptions = [[
-        Move Lyric Baselines Up
-        Reset Lyric Baselines
-        Move Expression Baseline Above Down
-        Move Expression Baseline Above Up
-        Reset Expression Baseline Above
-        Move Expression Baseline Below Down
-        Move Expression Baseline Below Up
-        Reset Expression Baseline Below
-        Move Chord Baseline Down
-        Move Chord Baseline Up
-        Reset Chord Baseline
-        Move Fretboard Baseline Down
-        Move Fretboard Baseline Up
-        Reset Fretboard Baseline
-    ]]
-    finaleplugin.AdditionalDescriptions = [[
-        Moves all lyrics baselines up one space in the selected systems
-        Resets all selected lyrics baselines to default
-        Moves the selected expression above baseline down one space
-        Moves the selected expression above baseline up one space
-        Resets the selected expression above baselines
-        Moves the selected expression below baseline down one space
-        Moves the selected expression below baseline up one space
-        Resets the selected expression below baselines
-        Moves the selected chord baseline down one space
-        Moves the selected chord baseline up one space
-        Resets the selected chord baselines
-        Moves the selected fretboard baseline down one space
-        Moves the selected fretboard baseline up one space
-        Resets the selected fretboard baselines
-    ]]
+    finaleplugin.AdditionalMenuOptions = t.addl_menus
+    finaleplugin.AdditionalDescriptions = t.addl_descs
     finaleplugin.AdditionalPrefixes = [[
         direction = 1 -- no baseline_types table, which picks up the default (lyrics)
         direction = 0 -- no baseline_types table, which picks up the default (lyrics)
@@ -77,7 +121,7 @@ function plugindef()
         direction = 1 baseline_types = {finale.BASELINEMODE_FRETBOARD}
         direction = 0 baseline_types = {finale.BASELINEMODE_FRETBOARD}
     ]]
-    return "Move Lyric Baselines Down", "Move Lyrics Baselines Down", "Moves all lyrics baselines down one space in the selected systems"
+    return  t.menu, t.menu, t.desc
 end
 
 local configuration = require("library.configuration")

From 9311b4a1f0979c5c1036d05419b572a5b4fef934 Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Sun, 4 Feb 2024 17:42:51 -0600
Subject: [PATCH 44/61] Add script group name and description

---
 src/baseline_move_reset.lua | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/baseline_move_reset.lua b/src/baseline_move_reset.lua
index 118eb6a4..3017ee5e 100644
--- a/src/baseline_move_reset.lua
+++ b/src/baseline_move_reset.lua
@@ -103,6 +103,8 @@ function plugindef(locale)
 
         A value in a prefix overrides any setting in a configuration file.
     ]]
+    finaleplugin.ScriptGroupName = "Move or Reset Baselines"
+    finaleplugin.ScriptGroupDescription = "Move or reset baselines for systems in the selected region"
     finaleplugin.AdditionalMenuOptions = t.addl_menus
     finaleplugin.AdditionalDescriptions = t.addl_descs
     finaleplugin.AdditionalPrefixes = [[

From 983a675849438cc5748ccb8df1a4ce8cab421f08 Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Mon, 5 Feb 2024 04:14:59 -0600
Subject: [PATCH 45/61] minor updates

---
 src/baseline_move_reset.lua     | 2 +-
 utilities/localization_tool.lua | 6 +-----
 2 files changed, 2 insertions(+), 6 deletions(-)

diff --git a/src/baseline_move_reset.lua b/src/baseline_move_reset.lua
index 3017ee5e..a2a1d169 100644
--- a/src/baseline_move_reset.lua
+++ b/src/baseline_move_reset.lua
@@ -103,7 +103,7 @@ function plugindef(locale)
 
         A value in a prefix overrides any setting in a configuration file.
     ]]
-    finaleplugin.ScriptGroupName = "Move or Reset Baselines"
+    finaleplugin.ScriptGroupName = "Move or Reset System Baselines"
     finaleplugin.ScriptGroupDescription = "Move or reset baselines for systems in the selected region"
     finaleplugin.AdditionalMenuOptions = t.addl_menus
     finaleplugin.AdditionalDescriptions = t.addl_descs
diff --git a/utilities/localization_tool.lua b/utilities/localization_tool.lua
index f6adf237..4a1cb903 100644
--- a/utilities/localization_tool.lua
+++ b/utilities/localization_tool.lua
@@ -491,13 +491,9 @@ local function create_dialog()
     local lang_list = dlg:CreateComboBox(0, curr_y, "lang_list")
         :SetWidth(0)
         :DoAutoResizeWidth()
-    local lang_index = 0
+        :SetText("Spanish")
     for lang, _ in pairsbykeys(finale_supported_languages) do
         lang_list:AddString(finale.FCString(lang))
-        if lang == "Spanish" then
-            lang_list:SetSelectedItem(lang_index)
-        end
-        lang_index = lang_index + 1
     end
     curr_y = curr_y + button_height
     --editor

From 9a46284d0f836888cfc688503cfe580d2a6fe38d Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Mon, 5 Feb 2024 08:30:48 -0600
Subject: [PATCH 46/61] Added table to list of types recognized by AddStrings
 method. Other mods to take advantage of this.

---
 src/library/utils.lua           | 17 ++++++++
 src/mixin/FCMCtrlComboBox.lua   | 76 +++++++++++++++++++++++++++++++++
 src/mixin/FCMCtrlPopup.lua      |  6 ++-
 src/mixin/FCMCtrlStatic.lua     |  2 +-
 utilities/localization_tool.lua |  8 ++--
 5 files changed, 101 insertions(+), 8 deletions(-)
 create mode 100644 src/mixin/FCMCtrlComboBox.lua

diff --git a/src/library/utils.lua b/src/library/utils.lua
index 581f690d..4f3d34c4 100644
--- a/src/library/utils.lua
+++ b/src/library/utils.lua
@@ -63,6 +63,23 @@ function utils.iterate_keys(t)
     end
 end
 
+--[[
+% get_keys
+
+Returns an ordered array table of all the keys in a table.
+
+@ t (table)
+: (table) array table of the keys
+]]
+function utils.get_keys(t)
+    local retval = {}
+
+    for k, _ in pairsbykeys(t) do
+        table.insert(retval, k)
+    end
+    return retval
+end
+
 --[[
 % round
 
diff --git a/src/mixin/FCMCtrlComboBox.lua b/src/mixin/FCMCtrlComboBox.lua
new file mode 100644
index 00000000..8a6f3166
--- /dev/null
+++ b/src/mixin/FCMCtrlComboBox.lua
@@ -0,0 +1,76 @@
+--  Author: Robert Patterson
+--  Date: February 5, 2024
+--[[
+$module FCMCtrlComboBox
+
+The PDK offers FCCtrlCombox which is an edit box with a pulldown menu attached. It has the following
+features:
+
+- It is an actual subclass of FCCtrlEdit, which means FCMCtrlComboBox is a subclass of FCMCtrlEdit.
+- The text contents of the control does not have to match any of the pulldown values.
+
+The PDK manages the pulldown values and selectied item well enough for our purposes. Furthermore, the order in
+which you set text or set selected item matters as to which one you'll end up with when the window
+opens. The PDK takes the approach that setting text takes precedence over setting the selected item.
+For that reason, this module (at least for now) does not manage those properties separately.
+
+## Summary of Modifications
+- Overrode `AddString` to allows Lua `string` or `number` in addition to `FCString`.
+- Added `AddStrings` that accepts multiple arguments of `table`, `FCString`, Lua `string`, or `number`.
+]] --
+local mixin = require("library.mixin") -- luacheck: ignore
+local mixin_helper = require("library.mixin_helper") -- luacheck: ignore
+
+local class = {Methods = {}}
+local methods = class.Methods
+
+local temp_str = finale.FCString()
+
+--[[
+% AddString
+
+**[Fluid] [Override]**
+
+Override Changes:
+- Accepts Lua `string` or `number` in addition to `FCString`.
+- Hooks into control state preservation.
+
+@ self (FCMCtrlPopup)
+@ str (FCString | string | number)
+]]
+
+function methods:AddString(str)
+    mixin_helper.assert_argument_type(2, str, "string", "number", "FCString")
+
+    str = mixin_helper.to_fcstring(str, temp_str)
+    self:AddString__(str)
+end
+
+--[[
+% AddStrings
+
+**[Fluid]**
+
+Adds multiple strings to the popup.
+
+@ self (FCMCtrlComboBox)
+@ ... (table | FCStrings | FCString | string | number)
+]]
+function methods:AddStrings(...)
+    for i = 1, select("#", ...) do
+        local v = select(i, ...)
+        mixin_helper.assert_argument_type(i + 1, v, "table", "string", "number", "FCString", "FCStrings")
+
+        if type(v) == "userdata" and v:ClassName() == "FCStrings" then
+            for str in each(v) do
+                mixin.FCMCtrlComboBox.AddString(self, str)
+            end
+        elseif type(v) == "table" then
+            self:AddStrings(table.unpack(v))
+        else
+            mixin.FCMCtrlComboBox.AddString(self, v)
+        end
+    end
+end
+
+return class
diff --git a/src/mixin/FCMCtrlPopup.lua b/src/mixin/FCMCtrlPopup.lua
index cc48a120..04be0b44 100644
--- a/src/mixin/FCMCtrlPopup.lua
+++ b/src/mixin/FCMCtrlPopup.lua
@@ -247,17 +247,19 @@ end
 Adds multiple strings to the popup.
 
 @ self (FCMCtrlPopup)
-@ ... (FCStrings | FCString | string | number)
+@ ... (table, FCStrings | FCString | string | number)
 ]]
 function methods:AddStrings(...)
     for i = 1, select("#", ...) do
         local v = select(i, ...)
-        mixin_helper.assert_argument_type(i + 1, v, "string", "number", "FCString", "FCStrings")
+        mixin_helper.assert_argument_type(i + 1, v, "table", "string", "number", "FCString", "FCStrings")
 
         if type(v) == "userdata" and v:ClassName() == "FCStrings" then
             for str in each(v) do
                 mixin.FCMCtrlPopup.AddString(self, str)
             end
+        elseif type(v) == "table" then
+            self:AddStrings(table.unpack(v))
         else
             mixin.FCMCtrlPopup.AddString(self, v)
         end
diff --git a/src/mixin/FCMCtrlStatic.lua b/src/mixin/FCMCtrlStatic.lua
index 9a0a9e54..8df5d28f 100644
--- a/src/mixin/FCMCtrlStatic.lua
+++ b/src/mixin/FCMCtrlStatic.lua
@@ -220,7 +220,7 @@ If using the parent window's measurement unit, it will be automatically updated
 @ value (number) Value in 10000ths of an EVPU
 @ [measurementunit] (number | nil) Forces the value to be displayed in this measurement unit. Can only be omitted if parent window is `FCMCustomLuaWindow`.
 ]]
-function methods:SetMeasurementEfix(value, measurementunit)
+function methods:SetMeasurement10000th(value, measurementunit)
     mixin_helper.assert_argument_type(2, value, "number")
     mixin_helper.assert_argument_type(3, measurementunit, "number", "nil")
 
diff --git a/utilities/localization_tool.lua b/utilities/localization_tool.lua
index 4a1cb903..32e16495 100644
--- a/utilities/localization_tool.lua
+++ b/utilities/localization_tool.lua
@@ -488,13 +488,11 @@ local function create_dialog()
         :SetWidth((2 * editor_width) / 3)
         :AssureNoHorizontalOverlap(dlg:GetControl("open"), x_separator)
         :AddHandleCommand(on_popup)
-    local lang_list = dlg:CreateComboBox(0, curr_y, "lang_list")
+    dlg:CreateComboBox(0, curr_y, "lang_list")
         :SetWidth(0)
         :DoAutoResizeWidth()
+        :AddStrings(utils.get_keys(finale_supported_languages))
         :SetText("Spanish")
-    for lang, _ in pairsbykeys(finale_supported_languages) do
-        lang_list:AddString(finale.FCString(lang))
-    end
     curr_y = curr_y + button_height
     --editor
     curr_y = curr_y + y_separator
@@ -509,7 +507,7 @@ local function create_dialog()
         :SetConvertTabsToSpaces(#tab_str)
         :SetAutomaticallyIndent(true)
         :AddHandleCommand(on_text_change)
-    lang_list:HorizontallyAlignRightWith(editor, utils.win_mac(0, -3))
+        :HorizontallyAlignRightWith(dlg:GetControl("lang_list"), utils.win_mac(0, -3))
     curr_y = curr_y + editor_height
     -- command buttons
     curr_y = curr_y + y_separator

From 89423a09393814403a339baff92756e5ee8af1f8 Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Mon, 5 Feb 2024 19:17:13 -0600
Subject: [PATCH 47/61] refactor mixins per suggestions from ThistleSifter

---
 src/library/mixin_helper.lua      | 79 +++++++++++++++++++++++++++++++
 src/library/utils.lua             | 22 ++++++++-
 src/lyrics_baseline_reset.lua     | 54 ---------------------
 src/mixin/FCMControl.lua          |  7 +--
 src/mixin/FCMCtrlComboBox.lua     | 54 +++++++++++++--------
 src/mixin/FCMCtrlPopup.lua        | 37 ++++++---------
 src/mixin/FCMUI.lua               | 10 +---
 src/transpose_by_step.lua         | 28 +++++------
 src/transpose_chromatic.lua       | 30 ++++++------
 src/transpose_enharmonic_down.lua | 28 +++++------
 src/transpose_enharmonic_up.lua   | 28 +++++------
 utilities/localization_tool.lua   |  2 +-
 12 files changed, 209 insertions(+), 170 deletions(-)
 delete mode 100644 src/lyrics_baseline_reset.lua

diff --git a/src/library/mixin_helper.lua b/src/library/mixin_helper.lua
index 66b67040..5452352b 100644
--- a/src/library/mixin_helper.lua
+++ b/src/library/mixin_helper.lua
@@ -10,6 +10,7 @@ require("library.lua_compatibility")
 local utils = require("library.utils")
 local mixin = require("library.mixin")
 local library = require("library.general_library")
+local localization = require("library.localization")
 
 local mixin_helper = {}
 
@@ -561,6 +562,24 @@ function mixin_helper.to_fcstring(value, fcstr)
     return fcstr
 end
 
+--[[
+% to_string
+
+Casts a value to a Lua string. If the value is an `FCString`, it returns `LuaString`, otherwise it calls `tostring`.
+
+@ value (any)
+@ [fcstr] (FCString) An optional `FCString` object to populate to skip creating a new object.
+: (FCString)
+]]
+
+function mixin_helper.to_string(value)
+    if mixin_helper.is_instance_of(value, "FCString") then
+        return value.LuaString
+    end
+
+    return tostring(value)
+end
+
 --[[
 % boolean_to_error
 
@@ -578,4 +597,64 @@ function mixin_helper.boolean_to_error(object, method, ...)
     end
 end
 
+--[[
+% create_localized_proxy
+
+Creates a proxy method that takes localization keys instead of raw strings.
+
+@ method_name (string)
+@ class_name (string|nil) If `nil`, the resulting call will be on the `self` object. If a `string` is passed, it will be forwarded to a static call on that class in the `mixin` namespace.
+@ only_localize_args (table|nil) If `nil`, all values passed to the method will be localized. If only certain arguments need localizing, pass a `table` of argument `number`s (note that `self` is argument #1).
+: (function)
+]]
+function mixin_helper.create_localized_proxy(method_name, class_name, only_localize_args)
+    local args_to_localize
+    if only_localize_args == nil then
+        args_to_localize = setmetatable({}, { __index = function() return true end })
+    else
+        args_to_localize = utils.create_lookup_table(only_localize_args)
+    end
+
+    return function(self, ...)
+        local args = table.pack(...)
+
+        for arg_num = 1, args.n do
+            if args_to_localize[arg_num] then
+                mixin_helper.assert_argument_type(arg_num, args[arg_num], "string", "FCString")
+                args[arg_num] = localization.localize(mixin_helper.to_string(args[arg_num]))
+            end
+        end
+
+        --Tail call. Errors will pass through to the correct level
+        return (class_name and mixin[class_name] or self)[method_name](self, table.unpack(args, 1, args.n))
+    end
+end
+
+--[[
+% process_string_arguments
+
+Process multiple string arguments.
+
+@ self (class instance)
+@ method_func (function) A method on the class that accepts a single Lua `string` or `FCString` instance
+@ ... (table, FCStrings | FCString | string | number)
+]]
+function mixin_helper.process_string_arguments(self, method_func, ...)
+    for i = 1, select("#", ...) do
+        local v = select(i, ...)
+        mixin_helper.assert_argument_type(i + 1, v, "table", "string", "number", "FCString", "FCStrings")
+
+        if type(v) == "userdata" and v:ClassName() == "FCStrings" then
+            for str in each(v) do
+                method_func(self, str)
+            end
+        elseif type(v) == "table" then
+            mixin_helper.process_string_arguments(self, method_func, table.unpack(v))
+        else
+            method_func(self, v)
+        end
+    end
+end
+
+
 return mixin_helper
diff --git a/src/library/utils.lua b/src/library/utils.lua
index 4f3d34c4..cf8b3231 100644
--- a/src/library/utils.lua
+++ b/src/library/utils.lua
@@ -66,12 +66,12 @@ end
 --[[
 % get_keys
 
-Returns an ordered array table of all the keys in a table.
+Returns a sorted array table of all the keys in a table.
 
 @ t (table)
 : (table) array table of the keys
 ]]
-function utils.get_keys(t)
+function utils.create_keys_table(t)
     local retval = {}
 
     for k, _ in pairsbykeys(t) do
@@ -80,6 +80,24 @@ function utils.get_keys(t)
     return retval
 end
 
+--[[
+% create_lookup_table
+
+Creates a value lookup table from an existing table.
+
+@ t (table)
+: (table)
+]]
+function utils.create_lookup_table(t)
+    local lookup = {}
+
+    for _, v in pairs(t) do
+        lookup[v] = true
+    end
+
+    return lookup
+end
+
 --[[
 % round
 
diff --git a/src/lyrics_baseline_reset.lua b/src/lyrics_baseline_reset.lua
deleted file mode 100644
index e208bb81..00000000
--- a/src/lyrics_baseline_reset.lua
+++ /dev/null
@@ -1,54 +0,0 @@
-function plugindef()
-   -- This function and the 'finaleplugin' namespace
-   -- are both reserved for the plug-in definition.
-       finaleplugin.Author = "Jacob Winkler"
-    finaleplugin.Copyright = "2022"
-    finaleplugin.Version = "1.0.1"
-    finaleplugin.Date = "2022-10-20"
-    finaleplugin.RequireSelection = true
-    finaleplugin.AuthorEmail = "jacob.winkler@mac.com"
-   return "Reset Lyric Baselines (system specific)", "Reset Lyric Baselines (system specific)", "Resets Lyric Baselines on a system-by-system basis (3rd triangle)"
-end
-
-
-function lyrics_baseline_reset()
-    local region = finenv.Region()
-    local systems = finale.FCStaffSystems()
-    systems:LoadAll()
-
-    local start_measure = region:GetStartMeasure()
-    local end_measure = region:GetEndMeasure()
-    local system = systems:FindMeasureNumber(start_measure)
-    local lastSys = systems:FindMeasureNumber(end_measure)
-    local system_number = system:GetItemNo()
-    local lastSys_number = lastSys:GetItemNo()
-    local start_staff = region:GetStartStaff()
-    local end_staff = region:GetEndStaff()
-
-    for i = system_number, lastSys_number, 1 do
-        local baselines_verse = finale.FCBaselines()
-        local baselines_chorus = finale.FCBaselines()
-        local baselines_section = finale.FCBaselines()
-        local lyric_number = 1
-        baselines_verse:LoadAllForSystem(finale.BASELINEMODE_LYRICSVERSE, i)
-        baselines_chorus:LoadAllForSystem(finale.BASELINEMODE_LYRICSCHORUS, i)
-        baselines_section:LoadAllForSystem(finale.BASELINEMODE_LYRICSSECTION, i)
-        for j = start_staff, end_staff, 1 do
-            for k = lyric_number, 100, 1 do
-                local baseline_verse = baselines_verse:AssureSavedLyricNumber(finale.BASELINEMODE_LYRICSVERSE, i, j, k)
-                baseline_verse.VerticalOffset = 0
-                baseline_verse:Save()
-                --
-                local baseline_chorus = baselines_chorus:AssureSavedLyricNumber(finale.BASELINEMODE_LYRICSCHORUS, i, j, k)
-                baseline_chorus.VerticalOffset = 0
-                baseline_chorus:Save()
-                --
-                local baseline_section = baselines_section:AssureSavedLyricNumber(finale.BASELINEMODE_LYRICSSECTION, i, j, k)
-                baseline_section.VerticalOffset = 0
-                baseline_section:Save()
-            end
-        end
-    end
-end
-
-lyrics_baseline_reset()
\ No newline at end of file
diff --git a/src/mixin/FCMControl.lua b/src/mixin/FCMControl.lua
index c2e45969..bbe759d9 100644
--- a/src/mixin/FCMControl.lua
+++ b/src/mixin/FCMControl.lua
@@ -12,7 +12,6 @@ $module FCMControl
 ]] --
 local mixin = require("library.mixin")
 local mixin_helper = require("library.mixin_helper")
-local localization = require("library.localization")
 
 local class = {Methods = {}}
 local methods = class.Methods
@@ -406,10 +405,6 @@ Removes a handler added with `AddHandleCommand`.
 @ self (FCMControl)
 @ key (string) The key into the localization table. If there is no entry in the appropriate localization table, the key is the text.
 ]]
-function methods:SetTextLocalized(key)
-    mixin_helper.assert_argument_type(2, key, "string")
-
-    self:SetText(localization.localize(key))
-end
+methods.SetTextLocalized = mixin_helper.create_localized_proxy("SetText", "FCMControl")
 
 return class
diff --git a/src/mixin/FCMCtrlComboBox.lua b/src/mixin/FCMCtrlComboBox.lua
index 8a6f3166..332310bd 100644
--- a/src/mixin/FCMCtrlComboBox.lua
+++ b/src/mixin/FCMCtrlComboBox.lua
@@ -10,16 +10,17 @@ features:
 - The text contents of the control does not have to match any of the pulldown values.
 
 The PDK manages the pulldown values and selectied item well enough for our purposes. Furthermore, the order in
-which you set text or set selected item matters as to which one you'll end up with when the window
+which you set text or set the selected item matters as to which one you'll end up with when the window
 opens. The PDK takes the approach that setting text takes precedence over setting the selected item.
 For that reason, this module (at least for now) does not manage those properties separately.
 
 ## Summary of Modifications
 - Overrode `AddString` to allows Lua `string` or `number` in addition to `FCString`.
 - Added `AddStrings` that accepts multiple arguments of `table`, `FCString`, Lua `string`, or `number`.
+- Added localized versions `AddStringLocalized` and `AddStringsLocalized`.
 ]] --
-local mixin = require("library.mixin") -- luacheck: ignore
-local mixin_helper = require("library.mixin_helper") -- luacheck: ignore
+local mixin = require("library.mixin")
+local mixin_helper = require("library.mixin_helper")
 
 local class = {Methods = {}}
 local methods = class.Methods
@@ -46,31 +47,44 @@ function methods:AddString(str)
     self:AddString__(str)
 end
 
+--[[
+% AddStringLocalized
+
+**[Fluid]**
+
+Localized version of `AddString`.
+
+@ self (FCMControl)
+@ key (string | FCString) The key into the localization table. If there is no entry in the appropriate localization table, the key is the text.
+]]
+methods.AddStringLocalized = mixin_helper.create_localized_proxy("AddString")
+
 --[[
 % AddStrings
 
 **[Fluid]**
 
-Adds multiple strings to the popup.
+Adds multiple strings to the combobox.
 
-@ self (FCMCtrlComboBox)
-@ ... (table | FCStrings | FCString | string | number)
+@ self (FCMCtrlPopup)
+@ ... (table, FCStrings | FCString | string | number)
 ]]
 function methods:AddStrings(...)
-    for i = 1, select("#", ...) do
-        local v = select(i, ...)
-        mixin_helper.assert_argument_type(i + 1, v, "table", "string", "number", "FCString", "FCStrings")
-
-        if type(v) == "userdata" and v:ClassName() == "FCStrings" then
-            for str in each(v) do
-                mixin.FCMCtrlComboBox.AddString(self, str)
-            end
-        elseif type(v) == "table" then
-            self:AddStrings(table.unpack(v))
-        else
-            mixin.FCMCtrlComboBox.AddString(self, v)
-        end
-    end
+    mixin_helper.process_string_arguments(self, mixin.FCMCtrlComboBox.AddString, ...)
+end
+
+--[[
+% AddStrings
+
+**[Fluid]**
+
+Adds multiple localized strings to the combobox.
+
+@ self (FCMCtrlPopup)
+@ ... (table, FCStrings | FCString | string | number)
+]]
+function methods:AddStringsLocalized(...)
+    mixin_helper.process_string_arguments(self, mixin.FCMCtrlComboBox.AddStringLocalized, ...)
 end
 
 return class
diff --git a/src/mixin/FCMCtrlPopup.lua b/src/mixin/FCMCtrlPopup.lua
index 04be0b44..09cbb09b 100644
--- a/src/mixin/FCMCtrlPopup.lua
+++ b/src/mixin/FCMCtrlPopup.lua
@@ -7,6 +7,7 @@ $module FCMCtrlPopup
 - Setters that accept `FCString` will also accept a Lua `string` or `number`.
 - `FCString` parameter in getters is optional and if omitted, the result will be returned as a Lua `string`.
 - Setters that accept `FCStrings` will also accept multiple arguments of `FCString`, Lua `string`, or `number`.
+- Added `AddStrings` that accepts multiple arguments of `table`, `FCString`, Lua `string`, or `number`.
 - Added numerous methods for accessing and modifying popup items.
 - Added `SelectionChange` custom control event.
 - Added hooks for preserving control state
@@ -14,7 +15,6 @@ $module FCMCtrlPopup
 local mixin = require("library.mixin")
 local mixin_helper = require("library.mixin_helper")
 local utils = require("library.utils")
-local localization = require("library.localization")
 
 local class = {Methods = {}}
 local methods = class.Methods
@@ -239,6 +239,18 @@ function methods:AddString(str)
     table.insert(private[self].Items, str.LuaString)
 end
 
+--[[
+% AddStringLocalized
+
+**[Fluid]**
+
+Localized version of `AddString`.
+
+@ self (FCMControl)
+@ key (string | FCString) The key into the localization table. If there is no entry in the appropriate localization table, the key is the text.
+]]
+methods.AddStringLocalized = mixin_helper.create_localized_proxy("AddString")
+
 --[[
 % AddStrings
 
@@ -250,23 +262,9 @@ Adds multiple strings to the popup.
 @ ... (table, FCStrings | FCString | string | number)
 ]]
 function methods:AddStrings(...)
-    for i = 1, select("#", ...) do
-        local v = select(i, ...)
-        mixin_helper.assert_argument_type(i + 1, v, "table", "string", "number", "FCString", "FCStrings")
-
-        if type(v) == "userdata" and v:ClassName() == "FCStrings" then
-            for str in each(v) do
-                mixin.FCMCtrlPopup.AddString(self, str)
-            end
-        elseif type(v) == "table" then
-            self:AddStrings(table.unpack(v))
-        else
-            mixin.FCMCtrlPopup.AddString(self, v)
-        end
-    end
+    mixin_helper.process_string_arguments(self, mixin.FCMCtrlPopup.AddString, ...)
 end
 
-
 --[[
 % AddStringsLocalized
 
@@ -278,12 +276,7 @@ Adds multiple localized strings to the popup.
 @ ... (string) keys of strings to be added. If no localization is found, the key is added.
 ]]
 function methods:AddStringsLocalized(...)
-    for i = 1, select("#", ...) do
-        local v = select(i, ...)
-        mixin_helper.assert_argument_type(i + 1, v, "string")
-
-        mixin.FCMCtrlPopup.AddString(self, localization.localize(v))
-    end
+    mixin_helper.process_string_arguments(self, mixin.FCMCtrlPopup.AddStringLocalized, ...)
 end
 
 --[[
diff --git a/src/mixin/FCMUI.lua b/src/mixin/FCMUI.lua
index 31e834b3..d0b4e02d 100644
--- a/src/mixin/FCMUI.lua
+++ b/src/mixin/FCMUI.lua
@@ -8,7 +8,6 @@ $module FCMUI
 ]] --
 local mixin = require("library.mixin") -- luacheck: ignore
 local mixin_helper = require("library.mixin_helper")
-local localization = require("library.localization")
 
 local class = {Methods = {}}
 local methods = class.Methods
@@ -76,17 +75,12 @@ end
 
 **[Fluid]**
 
-Displays a localized error message. 
+Displays a localized error message.
 
 @ self (FCMControl)
 @ message_key (string) The key into the localization table. If there is no entry in the appropriate localization table, the key is the message.
 @ title_key (string) The key into the localization table. If there is no entry in the appropriate localization table, the key is the title.
 ]]
-function methods:AlertErrorLocalized(message_key, title_key)
-    mixin_helper.assert_argument_type(2, message_key, "string")
-    mixin_helper.assert_argument_type(3, title_key, "string")
-
-    self:AlertError(localization.localize(message_key), localization.localize(title_key))
-end
+methods.AlertErrorLocalized = mixin_helper.create_localized_proxy("AlertError")
 
 return class
diff --git a/src/transpose_by_step.lua b/src/transpose_by_step.lua
index d3472bfd..293629a3 100644
--- a/src/transpose_by_step.lua
+++ b/src/transpose_by_step.lua
@@ -1,4 +1,18 @@
 function plugindef(locale)
+    local loc = {}
+    loc.en = {
+        menu = "Transpose By Steps",
+        desc = "Transpose by the number of steps given, simplifying the note spelling as needed."
+    }
+    loc.es = {
+        menu = "Trasponer por pasos",
+        desc = "Trasponer por el número de pasos dado, simplificando la enarmonización según sea necesario.",
+    }
+    loc.de = {
+        menu = "Transponieren nach Schritten",
+        desc = "Transponieren nach der angegebenen Anzahl von Schritten und vereinfachen die Notation nach Bedarf.",
+    }
+    local t = locale and loc[locale:sub(1,2)] or loc.en
     finaleplugin.RequireSelection = false
     finaleplugin.HandlesUndo = true -- not recognized by JW Lua or RGP Lua v0.55
     finaleplugin.Author = "Robert Patterson"
@@ -30,20 +44,6 @@ function plugindef(locale)
         Later versions of RGP Lua (0.58 or higher) ignore this configuration file (if it exists) and read the correct
         information from the Finale document.
     ]]
-    local loc = {}
-    loc.en = {
-        menu = "Transpose By Steps",
-        desc = "Transpose by the number of steps given, simplifying the note spelling as needed."
-    }
-    loc.es = {
-        menu = "Trasponer por pasos",
-        desc = "Trasponer por el número de pasos dado, simplificando la enarmonización según sea necesario.",
-    }
-    loc.de = {
-        menu = "Transponieren nach Schritten",
-        desc = "Transponieren nach der angegebenen Anzahl von Schritten und vereinfachen die Notation nach Bedarf.",
-    }
-    local t = locale and loc[locale:sub(1,2)] or loc.en
     return t.menu .. "...", t.menu, t.desc
 end
 
diff --git a/src/transpose_chromatic.lua b/src/transpose_chromatic.lua
index 65f01f4f..b16af8c2 100644
--- a/src/transpose_chromatic.lua
+++ b/src/transpose_chromatic.lua
@@ -1,4 +1,18 @@
 function plugindef(locale)
+    local loc = {}
+    loc.en = {
+        menu = "Transpose Chromatic",
+        desc = "Chromatic transposition of selected region (supports microtone systems)."
+    }
+    loc.es = {
+        menu = "Trasponer cromático",
+        desc = "Trasposición cromática de la región seleccionada (soporta sistemas de microtono)."
+    }
+    loc.de = {
+        menu = "Transponieren chromatisch",
+        desc = "Chromatische Transposition des ausgewählten Abschnittes (unterstützt Mikrotonsysteme)."
+    }
+    local t = locale and loc[locale:sub(1,2)] or loc.en
     finaleplugin.RequireSelection = false
     finaleplugin.HandlesUndo = true -- not recognized by JW Lua or RGP Lua v0.55
     finaleplugin.Author = "Robert Patterson"
@@ -29,20 +43,6 @@ function plugindef(locale)
         Later versions of RGP Lua (0.58 or higher) ignore this configuration file (if it exists) and read the correct
         information from the Finale document.
     ]]
-    local loc = {}
-    loc.en = {
-        menu = "Transpose Chromatic",
-        desc = "Chromatic transposition of selected region (supports microtone systems)."
-    }
-    loc.es = {
-        menu = "Trasponer cromático",
-        desc = "Trasposición cromática de la región seleccionada (soporta sistemas de microtono)."
-    }
-    loc.de = {
-        menu = "Transponieren chromatisch",
-        desc = "Chromatische Transposition des ausgewählten Abschnittes (unterstützt Mikrotonsysteme)."
-    }
-    local t = locale and loc[locale:sub(1,2)] or loc.en
     return t.menu .. "...", t.menu, t.desc
 end
 
@@ -124,7 +124,7 @@ function do_transpose_chromatic(direction, interval_index, simplify, plus_octave
         finenv.StartNewUndoBlock(undostr, true) -- JW Lua automatically terminates the final undo block we start here
     end
     if not success then
-        global_dialog:CreateUI():AlertErrorLocalized(
+        global_dialog:CreateChildUI():AlertErrorLocalized(
             "Finale is unable to represent some of the transposed pitches. These pitches were left unchanged.",
             "Transposition Error"
         )
diff --git a/src/transpose_enharmonic_down.lua b/src/transpose_enharmonic_down.lua
index 34fd86c0..c39c078e 100644
--- a/src/transpose_enharmonic_down.lua
+++ b/src/transpose_enharmonic_down.lua
@@ -1,4 +1,18 @@
 function plugindef(locale)
+    local loc = {}
+    loc.en = {
+        menu = "Enharmonic Transpose Down",
+        desc = "Transpose down enharmonically all notes in the selected region."
+    }
+    loc.es = {
+        menu = "Trasposición enarmónica hacia abajo",
+        desc = "Trasponer hacia abajo enarmónicamente todas las notas en la región seleccionada.",
+    }
+    loc.de = {
+        menu = "Enharmonische Transposition nach unten",
+        desc = "Transponieren alle Noten im ausgewählten Abschnitt enharmonisch nach unten.",
+    }
+    local t = locale and loc[locale:sub(1,2)] or loc.en
     finaleplugin.RequireSelection = true
     finaleplugin.Author = "Robert Patterson"
     finaleplugin.Copyright = "CC0 https://creativecommons.org/publicdomain/zero/1.0/"
@@ -26,20 +40,6 @@ function plugindef(locale)
         Later versions of RGP Lua (0.58 or higher) ignore this configuration file (if it exists) and read the correct
         information from the Finale document.
     ]]
-    local loc = {}
-    loc.en = {
-        menu = "Enharmonic Transpose Down",
-        desc = "Transpose down enharmonically all notes in the selected region."
-    }
-    loc.es = {
-        menu = "Trasposición enarmónica hacia abajo",
-        desc = "Trasponer hacia abajo enarmónicamente todas las notas en la región seleccionada.",
-    }
-    loc.de = {
-        menu = "Enharmonische Transposition nach unten",
-        desc = "Transponieren alle Noten im ausgewählten Abschnitt enharmonisch nach unten.",
-    }
-    local t = locale and loc[locale:sub(1,2)] or loc.en
     return t.menu, t.menu, t.desc
 end
 
diff --git a/src/transpose_enharmonic_up.lua b/src/transpose_enharmonic_up.lua
index 857dadab..0f68dd75 100644
--- a/src/transpose_enharmonic_up.lua
+++ b/src/transpose_enharmonic_up.lua
@@ -1,4 +1,18 @@
 function plugindef(locale)
+    local loc = {}
+    loc.en = {
+        menu = "Enharmonic Transpose Up",
+        desc = "Transpose up enharmonically all notes in the selected region."
+    }
+    loc.es = {
+        menu = "Trasposición enarmónica hacia arriba",
+        desc = "Trasponer hacia arriba enarmónicamente todas las notas en la región seleccionada.",
+    }
+    loc.de = {
+        menu = "Enharmonische Transposition nach oben",
+        desc = "Transponieren alle Noten im ausgewählten Abschnitt enharmonisch nach oben.",
+    }
+    local t = locale and loc[locale:sub(1,2)] or loc.en
     finaleplugin.RequireSelection = true
     finaleplugin.Author = "Robert Patterson"
     finaleplugin.Copyright = "CC0 https://creativecommons.org/publicdomain/zero/1.0/"
@@ -26,20 +40,6 @@ function plugindef(locale)
         Later versions of RGP Lua (0.58 or higher) ignore this configuration file (if it exists) and read the correct
         information from the Finale document.
     ]]
-    local loc = {}
-    loc.en = {
-        menu = "Enharmonic Transpose Up",
-        desc = "Transpose up enharmonically all notes in the selected region."
-    }
-    loc.es = {
-        menu = "Trasposición enarmónica hacia arriba",
-        desc = "Trasponer hacia arriba enarmónicamente todas las notas en la región seleccionada.",
-    }
-    loc.de = {
-        menu = "Enharmonische Transposition nach oben",
-        desc = "Transponieren alle Noten im ausgewählten Abschnitt enharmonisch nach oben.",
-    }
-    local t = locale and loc[locale:sub(1,2)] or loc.en
     return t.menu, t.menu, t.desc
 end
 
diff --git a/utilities/localization_tool.lua b/utilities/localization_tool.lua
index 32e16495..44652944 100644
--- a/utilities/localization_tool.lua
+++ b/utilities/localization_tool.lua
@@ -491,7 +491,7 @@ local function create_dialog()
     dlg:CreateComboBox(0, curr_y, "lang_list")
         :SetWidth(0)
         :DoAutoResizeWidth()
-        :AddStrings(utils.get_keys(finale_supported_languages))
+        :AddStrings(utils.create_keys_table(finale_supported_languages))
         :SetText("Spanish")
     curr_y = curr_y + button_height
     --editor

From a2e17a068bfa7f86d58ff9de214b7c5615c7c7e8 Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Mon, 5 Feb 2024 19:29:08 -0600
Subject: [PATCH 48/61] a word

---
 utilities/localization_tool.lua | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/utilities/localization_tool.lua b/utilities/localization_tool.lua
index 44652944..a241d04d 100644
--- a/utilities/localization_tool.lua
+++ b/utilities/localization_tool.lua
@@ -14,7 +14,7 @@ function plugindef()
 
         Functions include:
 
-        - Automatically create a table of all quoted strings in the library. The user can then edit this
+        - Automatically create a table of all quoted strings in the script. The user can then edit this
             down to the user-facing strings that need to be localized.
         - Given a table of strings, creates a localization file for a specified language.
         - Create a localized `plugindef` function for a script.

From 0a3f419efa02c05099c9b218a3e2c0a214c9af3c Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Mon, 5 Feb 2024 19:51:43 -0600
Subject: [PATCH 49/61] update sample localized script with latest approaches

---
 samples/auto_layout.lua                   | 52 ++++++++++++-----------
 samples/auto_layout_localizing_script.lua | 32 --------------
 src/mixin/__FCMUserWindow.lua             | 12 ++++++
 3 files changed, 40 insertions(+), 56 deletions(-)
 delete mode 100644 samples/auto_layout_localizing_script.lua

diff --git a/samples/auto_layout.lua b/samples/auto_layout.lua
index 1b9b068c..14025efd 100644
--- a/samples/auto_layout.lua
+++ b/samples/auto_layout.lua
@@ -9,7 +9,13 @@ local mixin = require('library.mixin')
 local localization = require('library.localization')
 
 --
--- This table was auto-generated with localization_developer.create_localized_base_table_string(en)
+-- For scripts in the `src` directory, each localization should be separately stored in the
+-- `localization` subdirectory. See the comments in `library/localization.lua` for more details.
+-- The localization tables are included here in this sample to keep the sample self-contained.
+--
+
+--
+-- This table was auto-generated with `utilities/localization_tool.lua`
 -- Then it was edited to include only the strings that need to be localized.
 --
 localization.en = -- this is en_GB due to spelling of "Localisation"
@@ -38,9 +44,7 @@ localization.en_US =
 }
 
 --
--- The rest of the localization tables were created one-at-a-time with the auto_layout_localizing_script.lua
---
--- This table was auto-generated with localization_developer.translate_localized_table_string(localization.en, "en", "es")
+-- The rest of the localization tables were created one-at-a-time with the `utilities/localization_tool.lua` script.
 --
 localization.es = {
     ["Action Button"] = "Botón de Acción",
@@ -62,7 +66,7 @@ localization.es = {
 }
 
 --
--- This table was auto-generated with localization_developer.translate_localized_table_string(localization.en, "en", "es")
+-- This table was auto-generated with `utilities/localization_tool.lua`
 --
 localization.jp = {
     ["Action Button"] = "アクションボタン",
@@ -84,7 +88,7 @@ localization.jp = {
 }
 
 --
--- This table was auto-generated with localization_developer.translate_localized_table_string(localization.en, "en", "de")
+-- This table was auto-generated with `utilities/localization_tool.lua`
 --
 localization.de = {
     ["Action Button"] = "Aktionsknopf",
@@ -185,7 +189,7 @@ localization.set_locale("fa")
 
 function create_dialog()
     local dlg = mixin.FCXCustomLuaWindow()
-    dlg:SetTitle(localization.localize("Test Autolayout With Localisation"))
+    dlg:SetTitleLocalized("Test Autolayout With Localisation")
 
     local line_no = 0
     local y_increment = 22
@@ -196,7 +200,7 @@ function create_dialog()
     dlg:CreateStatic(0, line_no * y_increment, "option1-label")
         :DoAutoResizeWidth()
         :SetWidth(0)
-        :SetText(localization.localize("First Option"))
+        :SetTextLocalized("First Option")
     dlg:CreateEdit(0, line_no * y_increment - utils.win_mac(2, 3), "option1")
         :SetInteger(1)
         :AssureNoHorizontalOverlap(dlg:GetControl("option1-label"), label_edit_separ)
@@ -205,13 +209,13 @@ function create_dialog()
     dlg:CreateCheckbox(0, line_no * y_increment, "left-checkbox1")
         :DoAutoResizeWidth()
         :SetWidth(0)
-        :SetText(localization.localize("Left Checkbox Option 1"))
+        :SetTextLocalized("Left Checkbox Option 1")
     line_no = line_no + 1
 
     dlg:CreateStatic(0, line_no * y_increment, "option2-label")
         :DoAutoResizeWidth()
         :SetWidth(0)
-        :SetText(localization.localize("Second Option"))
+        :SetTextLocalized("Second Option")
     dlg:CreateEdit(10, line_no * y_increment - utils.win_mac(2, 3), "option2")
         :SetInteger(2)
         :AssureNoHorizontalOverlap(dlg:GetControl("option2-label"), label_edit_separ)
@@ -221,7 +225,7 @@ function create_dialog()
     dlg:CreateCheckbox(0, line_no * y_increment, "left-checkbox2")
         :DoAutoResizeWidth()
         :SetWidth(0)
-        :SetText(localization.localize("Left Checkbox Option 2"))
+        :SetTextLocalized("Left Checkbox Option 2")
     line_no = line_no + 1
 
     -- center vertical line
@@ -236,7 +240,7 @@ function create_dialog()
     dlg:CreateStatic(0, line_no * y_increment, "option3-label")
         :DoAutoResizeWidth()
         :SetWidth(0)
-        :SetText(localization.localize("Third Option"))
+        :SetTextLocalized("Third Option")
         :AssureNoHorizontalOverlap(vertical_line, center_padding)
     dlg:CreateEdit(0, line_no * y_increment - utils.win_mac(2, 3), "option3")
         :SetInteger(3)
@@ -246,7 +250,7 @@ function create_dialog()
     dlg:CreateCheckbox(0, line_no * y_increment, "right-checkbox1")
         :DoAutoResizeWidth()
         :SetWidth(0)
-        :SetText(localization.localize("Right Three-State Option"))
+        :SetTextLocalized("Right Three-State Option")
         :SetThreeStatesMode(true)
         :AssureNoHorizontalOverlap(vertical_line, center_padding)
     line_no = line_no + 1
@@ -254,7 +258,7 @@ function create_dialog()
     dlg:CreateStatic(0, line_no * y_increment, "option4-label")
         :DoAutoResizeWidth()
         :SetWidth(0)
-        :SetText(localization.localize("Fourth Option"))
+        :SetTextLocalized("Fourth Option")
         :AssureNoHorizontalOverlap(vertical_line, center_padding)
     dlg:CreateEdit(0, line_no * y_increment - utils.win_mac(2, 3), "option4")
         :SetInteger(4)
@@ -265,7 +269,7 @@ function create_dialog()
     dlg:CreateButton(0, line_no * y_increment)
         :DoAutoResizeWidth()
         :SetWidth(0)
-        :SetText(localization.localize("Action Button"))
+        :SetTextLocalized("Action Button")
         :AssureNoHorizontalOverlap(vertical_line, center_padding)
         :HorizontallyAlignRightWith(dlg:GetControl("option4"))
 --        :HorizontallyAlignRightWithFurthest()
@@ -281,16 +285,16 @@ function create_dialog()
     dlg:CreateStatic(0, line_no * y_increment, "popup_label")
         :DoAutoResizeWidth()
         :SetWidth(0)
-        :SetText(localization.localize("Menu"))
+        :SetTextLocalized("Menu")
     local ctrl_popup = dlg:CreatePopup(0, line_no * y_increment - utils.win_mac(2, 2), "popup")
         :DoAutoResizeWidth()
         :SetWidth(0)
         :AssureNoHorizontalOverlap(dlg:GetControl("popup_label"), label_edit_separ)
     for counter = 1, 3 do
         if counter == 3 then
-            ctrl_popup:AddString(finale.FCString(localization.localize("This is long menu text ") .. counter))
+            ctrl_popup:AddStringLocalized(localization.localize("This is long menu text ") .. counter)
         else
-            ctrl_popup:AddString(finale.FCString(localization.localize("Short ") .. counter))
+            ctrl_popup:AddStringLocalized(localization.localize("Short ") .. counter)
         end
     end
     ctrl_popup:SetSelectedItem(0)
@@ -299,7 +303,7 @@ function create_dialog()
     dlg:CreateStatic(0, line_no * y_increment, "cbobox_label")
         :DoAutoResizeWidth()
         :SetWidth(0)
-        :SetText(localization.localize("Choices"))
+        :SetTextLocalized("Choices")
     local ctrl_cbobox = dlg:CreateComboBox(0, line_no * y_increment - utils.win_mac(2, 3), "cbobox")
         :DoAutoResizeWidth()
         :SetWidth(40)
@@ -307,9 +311,9 @@ function create_dialog()
         :HorizontallyAlignLeftWith(ctrl_popup)
     for counter = 1, 3 do
         if counter == 3 then
-            ctrl_cbobox:AddString(finale.FCString(localization.localize("This is long text choice ") .. counter))
+            ctrl_cbobox:AddString(localization.localize("This is long text choice ") .. counter)
         else
-            ctrl_cbobox:AddString(finale.FCString(localization.localize("Short ") .. counter))
+            ctrl_cbobox:AddString(localization.localize("Short ") .. counter)
         end
     end
     ctrl_cbobox:SetSelectedItem(0)
@@ -324,16 +328,16 @@ function create_dialog()
             :AssureNoHorizontalOverlap(ctrl_popup, 10)
             :AssureNoHorizontalOverlap(ctrl_cbobox, 10)
         if counter == 2 then
-            rbtn:SetText(finale.FCString(localization.localize("This is longer option text ") .. counter))
+            rbtn:SetTextLocalized(localization.localize("This is longer option text ") .. counter)
         else
-            rbtn:SetText(finale.FCString(localization.localize("Short ") .. counter))
+            rbtn:SetTextLocalized(localization.localize("Short ") .. counter)
         end
         counter = counter + 1
     end
     line_no = line_no + 2
 
     dlg:CreateCloseButton(0, line_no * y_increment + 5)
-        :SetText(localization.localize("Close"))
+        :SetTextLocalized("Close")
         :DoAutoResizeWidth()
         :HorizontallyAlignRightWithFurthest()
 
diff --git a/samples/auto_layout_localizing_script.lua b/samples/auto_layout_localizing_script.lua
deleted file mode 100644
index 5918159e..00000000
--- a/samples/auto_layout_localizing_script.lua
+++ /dev/null
@@ -1,32 +0,0 @@
-function plugindef()
-    finaleplugin.RequireDocument = false
-    finaleplugin.MinJWLuaVersion = 0.71
-    finaleplugin.ExecuteHttpsCalls = true
-end
-
-local localization = require("library.localization")
-
---
--- this table was copied from "auto_layout.lua" for the purpose of translating it
---
-localization.en =
-{
-    ["Action Button"] = "Action Button",
-    ["Choices"] = "Choices",
-    ["First Option"] = "First Option",
-    ["Fourth Option"] = "Fourth Option",
-    ["Left Checkbox Option 1"] = "Left Checkbox Option 1",
-    ["Left Checkbox Option 2"] = "Left Checkbox Option 2",
-    ["Menu"] = "Menu",
-    ["Right Three-State Option"] = "Right Three-State Option",
-    ["Second Option"] = "Second Option",
-    ["Short "] = "Short ",
-    ["Test Autolayout With Localisation"] = "Test Autolayout With Localisation",
-    ["Third Option"] = "Third Option",
-    ["This is long menu text "] = "This is long menu text ",
-    ["This is long text choice "] = "This is long text choice ",
-    ["This is longer option text "] = "This is longer option text ",
-}
-
-local ldev = require('library.localization_developer')
-ldev.translate_localized_table_string(localization.en, "en", "en_GB")
diff --git a/src/mixin/__FCMUserWindow.lua b/src/mixin/__FCMUserWindow.lua
index 8310b55c..c79fd8c6 100644
--- a/src/mixin/__FCMUserWindow.lua
+++ b/src/mixin/__FCMUserWindow.lua
@@ -60,6 +60,18 @@ function methods:SetTitle(title)
     self:SetTitle__(mixin_helper.to_fcstring(title, temp_str))
 end
 
+--[[
+% SetTitleLocalized
+
+Localized version of `SetTitle`.
+
+**[Fluid] [Override]**
+
+@ self (__FCMUserWindow)
+@ title (FCString | string | number)
+]]
+methods.SetTitleLocalized = mixin_helper.create_localized_proxy("SetTitle")
+
 --[[
 % CreateChildUI
 

From 7a3759e83ddc903f52ef32386b65c2aa70f0b195 Mon Sep 17 00:00:00 2001
From: Aaron Sherber <aaron@sherber.com>
Date: Tue, 6 Feb 2024 09:54:22 -0500
Subject: [PATCH 50/61] Bundle localizations

---
 .github/actions/bundle/dist/index.js      | 14 ++++++++++++--
 .github/actions/bundle/src/bundle.test.ts |  8 ++++----
 .github/actions/bundle/src/bundle.ts      | 16 +++++++++++++++-
 .github/actions/bundle/tsconfig.json      |  4 +++-
 .gitignore                                |  3 +++
 5 files changed, 37 insertions(+), 8 deletions(-)

diff --git a/.github/actions/bundle/dist/index.js b/.github/actions/bundle/dist/index.js
index 045b95a8..c6b617b3 100644
--- a/.github/actions/bundle/dist/index.js
+++ b/.github/actions/bundle/dist/index.js
@@ -6791,12 +6791,13 @@ const importFileBase = (name, importedFiles, fetcher) => {
     }
 };
 exports.importFileBase = importFileBase;
-const bundleFileBase = (name, importedFiles, mixins, fetcher) => {
+const bundleFileBase = (name, importedFiles, mixins, localizations, fetcher) => {
     var _a;
     const fileContents = (0, inject_extras_1.injectExtras)(name, fetcher(name));
     const fileStack = [fileContents];
     const importStack = (0, helpers_1.getAllImports)(fileContents);
     const importedFileNames = new Set();
+    importStack.push(...localizations);
     while (importStack.length > 0) {
         const nextImport = (_a = importStack.pop()) !== null && _a !== void 0 ? _a : '';
         if (importedFileNames.has(nextImport))
@@ -6821,7 +6822,16 @@ const bundleFileBase = (name, importedFiles, mixins, fetcher) => {
 };
 exports.bundleFileBase = bundleFileBase;
 const bundleFile = (name, sourcePath, mixins) => {
-    const bundled = (0, exports.bundleFileBase)(name, exports.files, mixins, (fileName) => fs_1.default.readFileSync(path_1.default.join(sourcePath, fileName)).toString());
+    const localizations = [];
+    const baseName = path_1.default.basename(name, '.lua');
+    const locPath = path_1.default.join(sourcePath, 'localization', baseName);
+    if (fs_1.default.existsSync(locPath)) {
+        localizations.push(...fs_1.default
+            .readdirSync(locPath)
+            .filter(fileName => fileName.endsWith('.lua'))
+            .map(file => `localization.${baseName}.${path_1.default.basename(file, '.lua')}`));
+    }
+    const bundled = (0, exports.bundleFileBase)(name, exports.files, mixins, localizations, (fileName) => fs_1.default.readFileSync(path_1.default.join(sourcePath, fileName)).toString());
     const parts = (0, helpers_1.getFileParts)(bundled);
     return (0, remove_comments_1.removeComments)(parts.prolog, true)
         + (0, remove_comments_1.removeComments)(parts.plugindef, false)
diff --git a/.github/actions/bundle/src/bundle.test.ts b/.github/actions/bundle/src/bundle.test.ts
index 1b590824..048c070e 100644
--- a/.github/actions/bundle/src/bundle.test.ts
+++ b/.github/actions/bundle/src/bundle.test.ts
@@ -40,7 +40,7 @@ describe('bundle', () => {
     }
 
     it('bundleFile', () => {
-        const bundle = bundleFileBase('a.lua', {}, [], fetcher)
+        const bundle = bundleFileBase('a.lua', {}, [], [], fetcher)
         expect(bundle).toBe(
             [
                 'package.preload["b"] = package.preload["b"] or function()',
@@ -58,17 +58,17 @@ describe('bundle', () => {
     })
 
     it('bundleFile with no imports', () => {
-        const bundle = bundleFileBase('c.lua', {}, [], fetcher)
+        const bundle = bundleFileBase('c.lua', {}, [], [], fetcher)
         expect(bundle).toBe('return {}')
     })
 
     it('ignore unresolvable imports', () => {
-        const bundle = bundleFileBase('invalid.lua', {}, [], fetcher)
+        const bundle = bundleFileBase('invalid.lua', {}, [], [], fetcher)
         expect(bundle).toBe(["local invalid = require('invalid.import')"].join('\n'))
     })
 
     it('imports all mixins', () => {
-        const bundle = bundleFileBase('mixin.lua', {}, ['mixin.FCMControl', 'mixin.FCMString'], fetcher)
+        const bundle = bundleFileBase('mixin.lua', {}, ['mixin.FCMControl', 'mixin.FCMString'], [], fetcher)
         expect(bundle).toBe(
             [
                 'package.preload["mixin.FCMControl"] = package.preload["mixin.FCMControl"] or function()',
diff --git a/.github/actions/bundle/src/bundle.ts b/.github/actions/bundle/src/bundle.ts
index eb1d21fa..b1a8edce 100644
--- a/.github/actions/bundle/src/bundle.ts
+++ b/.github/actions/bundle/src/bundle.ts
@@ -33,6 +33,7 @@ export const bundleFileBase = (
     name: string,
     importedFiles: ImportedFiles,
     mixins: string[],
+    localizations: string[],
     fetcher: (name: string) => string
 ) => {
     const fileContents = injectExtras(name, fetcher(name))
@@ -40,6 +41,8 @@ export const bundleFileBase = (
     const importStack: string[] = getAllImports(fileContents)
     const importedFileNames = new Set<string>()
 
+    importStack.push(...localizations)
+
     while (importStack.length > 0) {
         const nextImport = importStack.pop() ?? ''
         if (importedFileNames.has(nextImport)) continue
@@ -63,7 +66,18 @@ export const bundleFileBase = (
 }
 
 export const bundleFile = (name: string, sourcePath: string, mixins: string[]): string => {
-    const bundled: string = bundleFileBase(name, files, mixins, (fileName: string) =>
+    const localizations: string[] = []
+    const baseName = path.basename(name, '.lua')
+    const locPath = path.join(sourcePath, 'localization', baseName)
+    if (fs.existsSync(locPath)) {        
+        localizations.push(...fs
+            .readdirSync(locPath)
+            .filter(fileName => fileName.endsWith('.lua'))
+            .map(file => `localization.${baseName}.${path.basename(file, '.lua')}`)
+        )
+    }
+
+    const bundled: string = bundleFileBase(name, files, mixins, localizations, (fileName: string) =>
             fs.readFileSync(path.join(sourcePath, fileName)).toString()
         );
     const parts = getFileParts(bundled);
diff --git a/.github/actions/bundle/tsconfig.json b/.github/actions/bundle/tsconfig.json
index 83bb02d9..ab3e46f5 100644
--- a/.github/actions/bundle/tsconfig.json
+++ b/.github/actions/bundle/tsconfig.json
@@ -14,7 +14,9 @@
         "module": "commonjs",
         "moduleResolution": "node",
         "resolveJsonModule": true,
-        "declaration": false
+        "declaration": false,
+        "outDir": "dist",
+        "sourceMap": true
     },
     "include": [
         "**/*.ts",
diff --git a/.gitignore b/.gitignore
index 680628e2..9e31bd23 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,3 +30,6 @@ dist/**/personal*
 # (finale_lua_menus.txt is the menu layout file used by finale_lua_menu_organizer.lua)
 finale_lua_menus.txt
 
+.github/**/dist/*.js
+!.github/**/dist/index.js
+.github/**/dist/*.map
\ No newline at end of file

From 8ff3c8c4af578a43b1fe1bed1e7a30c276daef6c Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Tue, 6 Feb 2024 21:27:28 -0600
Subject: [PATCH 51/61] address code review issues.

---
 src/library/mixin_helper.lua    | 11 ++++------
 src/mixin/FCMCtrlComboBox.lua   | 16 +++++++-------
 src/mixin/FCMCtrlListBox.lua    | 37 +++++++++++++++++++++++----------
 src/mixin/FCMCtrlPopup.lua      |  6 +++---
 utilities/localization_tool.lua |  2 +-
 5 files changed, 42 insertions(+), 30 deletions(-)

diff --git a/src/library/mixin_helper.lua b/src/library/mixin_helper.lua
index 5452352b..993fdffd 100644
--- a/src/library/mixin_helper.lua
+++ b/src/library/mixin_helper.lua
@@ -558,7 +558,7 @@ function mixin_helper.to_fcstring(value, fcstr)
     end
 
     fcstr = fcstr or finale.FCString()
-    fcstr.LuaString = tostring(value)
+    fcstr.LuaString = value == nil and "" or tostring(value)
     return fcstr
 end
 
@@ -568,7 +568,6 @@ end
 Casts a value to a Lua string. If the value is an `FCString`, it returns `LuaString`, otherwise it calls `tostring`.
 
 @ value (any)
-@ [fcstr] (FCString) An optional `FCString` object to populate to skip creating a new object.
 : (FCString)
 ]]
 
@@ -577,7 +576,7 @@ function mixin_helper.to_string(value)
         return value.LuaString
     end
 
-    return tostring(value)
+    return value == nil and "" or tostring(value)
 end
 
 --[[
@@ -637,19 +636,17 @@ Process multiple string arguments.
 
 @ self (class instance)
 @ method_func (function) A method on the class that accepts a single Lua `string` or `FCString` instance
-@ ... (table, FCStrings | FCString | string | number)
+@ ... (FCStrings | FCString | string | number)
 ]]
 function mixin_helper.process_string_arguments(self, method_func, ...)
     for i = 1, select("#", ...) do
         local v = select(i, ...)
-        mixin_helper.assert_argument_type(i + 1, v, "table", "string", "number", "FCString", "FCStrings")
+        mixin_helper.assert_argument_type(i + 1, v, "string", "number", "FCString", "FCStrings")
 
         if type(v) == "userdata" and v:ClassName() == "FCStrings" then
             for str in each(v) do
                 method_func(self, str)
             end
-        elseif type(v) == "table" then
-            mixin_helper.process_string_arguments(self, method_func, table.unpack(v))
         else
             method_func(self, v)
         end
diff --git a/src/mixin/FCMCtrlComboBox.lua b/src/mixin/FCMCtrlComboBox.lua
index 332310bd..c0294cf3 100644
--- a/src/mixin/FCMCtrlComboBox.lua
+++ b/src/mixin/FCMCtrlComboBox.lua
@@ -36,7 +36,7 @@ Override Changes:
 - Accepts Lua `string` or `number` in addition to `FCString`.
 - Hooks into control state preservation.
 
-@ self (FCMCtrlPopup)
+@ self (FCMCtrlComboBox)
 @ str (FCString | string | number)
 ]]
 
@@ -54,8 +54,8 @@ end
 
 Localized version of `AddString`.
 
-@ self (FCMControl)
-@ key (string | FCString) The key into the localization table. If there is no entry in the appropriate localization table, the key is the text.
+@ self (FCMCtrlComboBox)
+@ key (string | FCString, number) The key into the localization table. If there is no entry in the appropriate localization table, the key is the text.
 ]]
 methods.AddStringLocalized = mixin_helper.create_localized_proxy("AddString")
 
@@ -66,22 +66,22 @@ methods.AddStringLocalized = mixin_helper.create_localized_proxy("AddString")
 
 Adds multiple strings to the combobox.
 
-@ self (FCMCtrlPopup)
-@ ... (table, FCStrings | FCString | string | number)
+@ self (FCMCtrlComboBox)
+@ ... (FCStrings | FCString | string | number)
 ]]
 function methods:AddStrings(...)
     mixin_helper.process_string_arguments(self, mixin.FCMCtrlComboBox.AddString, ...)
 end
 
 --[[
-% AddStrings
+% AddStringsLocalized
 
 **[Fluid]**
 
 Adds multiple localized strings to the combobox.
 
-@ self (FCMCtrlPopup)
-@ ... (table, FCStrings | FCString | string | number)
+@ self (FCMCtrlComboBox)
+@ ... (FCStrings | FCString | string | number) keys of strings to be added. If no localization is found, the key is added.
 ]]
 function methods:AddStringsLocalized(...)
     mixin_helper.process_string_arguments(self, mixin.FCMCtrlComboBox.AddStringLocalized, ...)
diff --git a/src/mixin/FCMCtrlListBox.lua b/src/mixin/FCMCtrlListBox.lua
index 087b7d5f..3c7aa63e 100644
--- a/src/mixin/FCMCtrlListBox.lua
+++ b/src/mixin/FCMCtrlListBox.lua
@@ -249,6 +249,18 @@ function methods:AddString(str)
     table.insert(private[self].Items, str.LuaString)
 end
 
+--[[
+% AddStringLocalized
+
+**[Fluid]**
+
+Localized version of `AddString`.
+
+@ self (FCMCtrlListBox)
+@ key (string | FCString, number) The key into the localization table. If there is no entry in the appropriate localization table, the key is the text.
+]]
+methods.AddStringLocalized = mixin_helper.create_localized_proxy("AddString")
+
 --[[
 % AddStrings
 
@@ -260,18 +272,21 @@ Adds multiple strings to the list box.
 @ ... (FCStrings | FCString | string | number)
 ]]
 function methods:AddStrings(...)
-    for i = 1, select("#", ...) do
-        local v = select(i, ...)
-        mixin_helper.assert_argument_type(i + 1, v, "string", "number", "FCString", "FCStrings")
+    mixin_helper.process_string_arguments(self, mixin.FCMCtrlListBox.AddString, ...)
+end
 
-        if type(v) == "userdata" and v:ClassName() == "FCStrings" then
-            for str in each(v) do
-                mixin.FCMCtrlListBox.AddString(self, str)
-            end
-        else
-            mixin.FCMCtrlListBox.AddString(self, v)
-        end
-    end
+--[[
+% AddStringsLocalized
+
+**[Fluid]**
+
+Adds multiple localized strings to the combobox.
+
+@ self (FCMCtrlListBox)
+@ ... (FCStrings | FCString | string | number) keys of strings to be added. If no localization is found, the key is added.
+]]
+function methods:AddStringsLocalized(...)
+    mixin_helper.process_string_arguments(self, mixin.FCMCtrlComboBox.AddStringLocalized, ...)
 end
 
 --[[
diff --git a/src/mixin/FCMCtrlPopup.lua b/src/mixin/FCMCtrlPopup.lua
index 09cbb09b..1b929e01 100644
--- a/src/mixin/FCMCtrlPopup.lua
+++ b/src/mixin/FCMCtrlPopup.lua
@@ -246,7 +246,7 @@ end
 
 Localized version of `AddString`.
 
-@ self (FCMControl)
+@ self (FCMCtrlPopup)
 @ key (string | FCString) The key into the localization table. If there is no entry in the appropriate localization table, the key is the text.
 ]]
 methods.AddStringLocalized = mixin_helper.create_localized_proxy("AddString")
@@ -259,7 +259,7 @@ methods.AddStringLocalized = mixin_helper.create_localized_proxy("AddString")
 Adds multiple strings to the popup.
 
 @ self (FCMCtrlPopup)
-@ ... (table, FCStrings | FCString | string | number)
+@ ... (FCStrings | FCString | string | number)
 ]]
 function methods:AddStrings(...)
     mixin_helper.process_string_arguments(self, mixin.FCMCtrlPopup.AddString, ...)
@@ -273,7 +273,7 @@ end
 Adds multiple localized strings to the popup.
 
 @ self (FCMCtrlPopup)
-@ ... (string) keys of strings to be added. If no localization is found, the key is added.
+@ ... (FCStrings | FCString | string | number) keys of strings to be added. If no localization is found, the key is added.
 ]]
 function methods:AddStringsLocalized(...)
     mixin_helper.process_string_arguments(self, mixin.FCMCtrlPopup.AddStringLocalized, ...)
diff --git a/utilities/localization_tool.lua b/utilities/localization_tool.lua
index a241d04d..e7e02e5f 100644
--- a/utilities/localization_tool.lua
+++ b/utilities/localization_tool.lua
@@ -491,7 +491,7 @@ local function create_dialog()
     dlg:CreateComboBox(0, curr_y, "lang_list")
         :SetWidth(0)
         :DoAutoResizeWidth()
-        :AddStrings(utils.create_keys_table(finale_supported_languages))
+        :AddStrings(table.unpack(utils.create_keys_table(finale_supported_languages)))
         :SetText("Spanish")
     curr_y = curr_y + button_height
     --editor

From 3bae1508d2b6df6bef2d6d1a4986142136cf12b9 Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Wed, 7 Feb 2024 08:06:36 -0600
Subject: [PATCH 52/61] in-progress checkpoint for Base localization.

---
 samples/auto_layout.lua                       | 37 +++-----
 src/baseline_move_reset.lua                   |  4 +-
 src/library/localization.lua                  | 48 +++++++---
 src/library/mixin_helper.lua                  |  2 +-
 src/localization/transpose_chromatic/Base.lua | 44 +++++++++
 src/localization/transpose_chromatic/de.lua   | 76 ++++++++-------
 src/localization/transpose_chromatic/es.lua   | 76 ++++++++-------
 src/transpose_by_step.lua                     | 10 +-
 src/transpose_chromatic.lua                   | 95 +++++++++----------
 src/transpose_enharmonic_down.lua             |  4 +-
 src/transpose_enharmonic_up.lua               |  4 +-
 utilities/localization_tool.lua               | 13 ++-
 12 files changed, 228 insertions(+), 185 deletions(-)
 create mode 100644 src/localization/transpose_chromatic/Base.lua

diff --git a/samples/auto_layout.lua b/samples/auto_layout.lua
index 14025efd..dbdda65a 100644
--- a/samples/auto_layout.lua
+++ b/samples/auto_layout.lua
@@ -198,8 +198,7 @@ function create_dialog()
 
     -- left side
     dlg:CreateStatic(0, line_no * y_increment, "option1-label")
-        :DoAutoResizeWidth()
-        :SetWidth(0)
+        :DoAutoResizeWidth(0)
         :SetTextLocalized("First Option")
     dlg:CreateEdit(0, line_no * y_increment - utils.win_mac(2, 3), "option1")
         :SetInteger(1)
@@ -207,14 +206,12 @@ function create_dialog()
     line_no = line_no + 1
 
     dlg:CreateCheckbox(0, line_no * y_increment, "left-checkbox1")
-        :DoAutoResizeWidth()
-        :SetWidth(0)
+        :DoAutoResizeWidth(0)
         :SetTextLocalized("Left Checkbox Option 1")
     line_no = line_no + 1
 
     dlg:CreateStatic(0, line_no * y_increment, "option2-label")
-        :DoAutoResizeWidth()
-        :SetWidth(0)
+        :DoAutoResizeWidth(0)
         :SetTextLocalized("Second Option")
     dlg:CreateEdit(10, line_no * y_increment - utils.win_mac(2, 3), "option2")
         :SetInteger(2)
@@ -223,8 +220,7 @@ function create_dialog()
     line_no = line_no + 1
 
     dlg:CreateCheckbox(0, line_no * y_increment, "left-checkbox2")
-        :DoAutoResizeWidth()
-        :SetWidth(0)
+        :DoAutoResizeWidth(0)
         :SetTextLocalized("Left Checkbox Option 2")
     line_no = line_no + 1
 
@@ -238,8 +234,7 @@ function create_dialog()
 
     -- right side
     dlg:CreateStatic(0, line_no * y_increment, "option3-label")
-        :DoAutoResizeWidth()
-        :SetWidth(0)
+        :DoAutoResizeWidth(0)
         :SetTextLocalized("Third Option")
         :AssureNoHorizontalOverlap(vertical_line, center_padding)
     dlg:CreateEdit(0, line_no * y_increment - utils.win_mac(2, 3), "option3")
@@ -248,16 +243,14 @@ function create_dialog()
     line_no = line_no + 1
 
     dlg:CreateCheckbox(0, line_no * y_increment, "right-checkbox1")
-        :DoAutoResizeWidth()
-        :SetWidth(0)
+        :DoAutoResizeWidth(0)
         :SetTextLocalized("Right Three-State Option")
         :SetThreeStatesMode(true)
         :AssureNoHorizontalOverlap(vertical_line, center_padding)
     line_no = line_no + 1
 
     dlg:CreateStatic(0, line_no * y_increment, "option4-label")
-        :DoAutoResizeWidth()
-        :SetWidth(0)
+        :DoAutoResizeWidth(0)
         :SetTextLocalized("Fourth Option")
         :AssureNoHorizontalOverlap(vertical_line, center_padding)
     dlg:CreateEdit(0, line_no * y_increment - utils.win_mac(2, 3), "option4")
@@ -268,7 +261,6 @@ function create_dialog()
 
     dlg:CreateButton(0, line_no * y_increment)
         :DoAutoResizeWidth()
-        :SetWidth(0)
         :SetTextLocalized("Action Button")
         :AssureNoHorizontalOverlap(vertical_line, center_padding)
         :HorizontallyAlignRightWith(dlg:GetControl("option4"))
@@ -283,12 +275,10 @@ function create_dialog()
     -- bottom side
     local start_line_no = line_no
     dlg:CreateStatic(0, line_no * y_increment, "popup_label")
-        :DoAutoResizeWidth()
-        :SetWidth(0)
+        :DoAutoResizeWidth(0)
         :SetTextLocalized("Menu")
     local ctrl_popup = dlg:CreatePopup(0, line_no * y_increment - utils.win_mac(2, 2), "popup")
-        :DoAutoResizeWidth()
-        :SetWidth(0)
+        :DoAutoResizeWidth(0)
         :AssureNoHorizontalOverlap(dlg:GetControl("popup_label"), label_edit_separ)
     for counter = 1, 3 do
         if counter == 3 then
@@ -301,12 +291,10 @@ function create_dialog()
     line_no = line_no + 1
 
     dlg:CreateStatic(0, line_no * y_increment, "cbobox_label")
-        :DoAutoResizeWidth()
-        :SetWidth(0)
+        :DoAutoResizeWidth(0)
         :SetTextLocalized("Choices")
     local ctrl_cbobox = dlg:CreateComboBox(0, line_no * y_increment - utils.win_mac(2, 3), "cbobox")
-        :DoAutoResizeWidth()
-        :SetWidth(40)
+        :DoAutoResizeWidth(40)
         :AssureNoHorizontalOverlap(dlg:GetControl("cbobox_label"), label_edit_separ)
         :HorizontallyAlignLeftWith(ctrl_popup)
     for counter = 1, 3 do
@@ -323,8 +311,7 @@ function create_dialog()
     local ctrl_radiobuttons = dlg:CreateRadioButtonGroup(0, line_no * y_increment, 3)
     local counter = 1
     for rbtn in each(ctrl_radiobuttons) do
-        rbtn:SetWidth(0)
-            :DoAutoResizeWidth()
+        rbtn:DoAutoResizeWidth(0)
             :AssureNoHorizontalOverlap(ctrl_popup, 10)
             :AssureNoHorizontalOverlap(ctrl_cbobox, 10)
         if counter == 2 then
diff --git a/src/baseline_move_reset.lua b/src/baseline_move_reset.lua
index a2a1d169..4d6b51a1 100644
--- a/src/baseline_move_reset.lua
+++ b/src/baseline_move_reset.lua
@@ -1,6 +1,6 @@
 function plugindef(locale)
     local loc = {}
-    loc.en = {
+    loc.Base = {
         addl_menus = [[
             Move Lyric Baselines Up
             Reset Lyric Baselines
@@ -72,7 +72,7 @@ function plugindef(locale)
         menu = "Mover las líneas de referencia de las letras hacia abajo",
         desc = "Mueve todas las líneas de referencia de las letras un espacio hacia abajo en los sistemas de pentagramas seleccionadas",
     }
-    local t = locale and loc[locale:sub(1, 2)] or loc.en
+    local t = locale and loc[locale:sub(1, 2)] or loc.Base
     finaleplugin.RequireSelection = true
     finaleplugin.Author = "Robert Patterson"
     finaleplugin.Version = "1.1"
diff --git a/src/library/localization.lua b/src/library/localization.lua
index 9d8e09a4..6c6729e9 100644
--- a/src/library/localization.lua
+++ b/src/library/localization.lua
@@ -6,8 +6,8 @@ a `plugindef` function, because the Lua plugin does not load any dependencies wh
 
 **Executive Summary**
 
-- Create language tables containing each user-facing string as key with a translation as the value.
-- Save them in the `localization` subdirectory as shown below.
+- Create language tables containing each user-facing string as a value with a key. The key can be any string value.
+- Save the language tables in the `localization` subdirectory as shown below.
 - Use the `...Localized` methods with `mixin` or if not using `mixin`, require the `localization`
 library directly and wrap any user-facing string in a call to `localization.localize`.
 
@@ -23,6 +23,7 @@ src/
     my_highly_useful_script.lua
     localization/
         my_highly_useful_script/
+            Base.lua
             de.lua
             es.lua
             es_ES.lua
@@ -33,16 +34,29 @@ src/
 
 Each localization lua should return a table of keys and translations.
 
-Japanase:
+Base:
+
+```
+--
+-- Base.lua:
+--
+local t = {
+    hello = "Hello",
+    goodbye = "Goodbye",
+    computer =  "Computer" 
+}
+
+
+Japanese:
 
 ```
 --
 -- jp.lua:
 --
 local t = {
-    ["Hello"] = "今日は",
-    ["Goodbye"] = "さようなら",
-    ["Computer"] =  "コンピュータ" 
+    hello = "今日は",
+    goodbye = "さようなら",
+    computer =  "コンピュータ" 
 }
 
 return t
@@ -55,9 +69,9 @@ Spanish:
 -- es.lua:
 --
 local t = {
-    ["Hello"] = "Hola",
-    ["Goodbye"] = "Adiós",
-    ["Computer"] = "Computadora"
+    hello = "Hola",
+    goodbye = "Adiós",
+    computer = "Computadora"
 }
 
 return t
@@ -71,16 +85,17 @@ differ from the the fallback language table.
 -- es_ES.lua:
 --
 local t = {
-    ["Computer"] = "Ordenador"
+    computer = "Ordenador"
 }
 
 return t
 ```
 
-The keys do not have to be in English, but they should be the same in all tables. It is not necessary to provide
-a table for the language the keys are in. That is, if the keys are in English, it is not necessary to provide `en.lua`.
-If you wish to add another language, you simply add it to the subfolder for the script, and no further action is
-required.
+The keys do not have to be user-friendly strings, but they should be the same in all tables. The recommended
+approach is to provide a `Base.lua` table that contains fallback translations if no others or available.
+For example, if you want your fallback language to be English, provide `Base.lua` with English translations rather
+than providing `en.lua`. Any time you wish to add another language, you simply add it to the subfolder for the script,
+and no further action is required.
 
 The `mixin` library provides automatic localization with the `...Localized` methods. Localized versions of user-facing
 text-based `mixin` methods should be added as needed, if they do not already exist. If your script does not require the
@@ -175,7 +190,12 @@ function localization.localize(input_string)
 
     if #locale > 2 then
         t = get_localized_table(locale:sub(1, 2))
+        if t and t[input_string] then
+            return t[input_string]
+        end
     end
+
+    t = get_localized_table("Base")
     
     return t and t[input_string] or input_string
 end
diff --git a/src/library/mixin_helper.lua b/src/library/mixin_helper.lua
index 993fdffd..3eaf1b76 100644
--- a/src/library/mixin_helper.lua
+++ b/src/library/mixin_helper.lua
@@ -568,7 +568,7 @@ end
 Casts a value to a Lua string. If the value is an `FCString`, it returns `LuaString`, otherwise it calls `tostring`.
 
 @ value (any)
-: (FCString)
+: (string)
 ]]
 
 function mixin_helper.to_string(value)
diff --git a/src/localization/transpose_chromatic/Base.lua b/src/localization/transpose_chromatic/Base.lua
new file mode 100644
index 00000000..ee407de1
--- /dev/null
+++ b/src/localization/transpose_chromatic/Base.lua
@@ -0,0 +1,44 @@
+--
+-- Localization Base.lua for transpose_chromatic.lua
+--
+local loc = {
+    error_msg_transposition = "Finale is unable to represent some of the transposed pitches. These pitches were left unchanged.",
+    augmented_fifth = "Augmented Fifth",
+    augmented_fourth = "Augmented Fourth",
+    augmented_second = "Augmented Second",
+    augmented_seventh = "Augmented Seventh",
+    augmented_sixth = "Augmented Sixth",
+    augmented_third = "Augmented Third",
+    augmented_unison = "Augmented Unison",
+    diminished_fifth = "Diminished Fifth",
+    diminished_fourth = "Diminished Fourth",
+    diminished_octave = "Diminished Octave",
+    diminished_second = "Diminished Second",
+    diminished_seventh = "Diminished Seventh",
+    diminished_sixth = "Diminished Sixth",
+    diminished_third = "Diminished Third",
+    direction = "Direction",
+    down = "Down",
+    interval = "Interval",
+    major_second = "Major Second",
+    major_seventh = "Major Seventh",
+    major_sixth = "Major Sixth",
+    major_third = "Major Third",
+    minor_second = "Minor Second",
+    minor_seventh = "Minor Seventh",
+    minor_sixth = "Minor Sixth",
+    minor_third = "Minor Third",
+    perfect_fifth = "Perfect Fifth",
+    perfect_fourth = "Perfect Fourth",
+    perfect_octave = "Perfect Octave",
+    perfect_unison = "Perfect Unison",
+    plus_octaves = "Plus Octaves",
+    preserve_existing = "Preserve Existing Notes",
+    simplify_spelling = "Simplify Spelling",
+    transposition_error = "Transposition Error",
+    up = "Up",
+    ok = "OK",
+    cancel = "Cancel",
+}
+
+return loc
diff --git a/src/localization/transpose_chromatic/de.lua b/src/localization/transpose_chromatic/de.lua
index 7b653a36..cd236e7e 100644
--- a/src/localization/transpose_chromatic/de.lua
+++ b/src/localization/transpose_chromatic/de.lua
@@ -2,45 +2,43 @@
 -- Localization de.lua for transpose_chromatic.lua
 --
 local loc = {
-    ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
-        "Finale kann einige der transponierten Tönhöhen nicht darstellen. Diese Tönhöhen wurden unverändert gelassen.",
-    ["Augmented Fifth"] = "Übermäßige Quinte",
-    ["Augmented Fourth"] = "Übermäßige Quarte",
-    ["Augmented Second"] = "Übermäßige Sekunde",
-    ["Augmented Seventh"] = "Übermäßige Septime",
-    ["Augmented Sixth"] = "Übermäßige Sexte",
-    ["Augmented Third"] = "Übermäßige Terz",
-    ["Augmented Unison"] = "Übermäßige Prime",
-    ["Diminished Fifth"] = "Verminderte Quinte",
-    ["Diminished Fourth"] = "Verminderte Quarte",
-    ["Diminished Octave"] = "Verminderte Oktave",
-    ["Diminished Second"] = "Verminderte Sekunde",
-    ["Diminished Seventh"] = "Verminderte Septime",
-    ["Diminished Sixth"] = "Verminderte Sexte",
-    ["Diminished Third"] = "Verminderte Terz",
-    ["Direction"] = "Richtung",
-    ["Down"] = "Runter",
-    ["Interval"] = "Intervall",
-    ["Major Second"] = "Große Sekunde",
-    ["Major Seventh"] = "Große Septime",
-    ["Major Sixth"] = "Große Sexte",
-    ["Major Third"] = "Große Terz",
-    ["Minor Second"] = "Kleine Sekunde",
-    ["Minor Seventh"] = "Kleine Septime",
-    ["Minor Sixth"] = "Kleine Sexte",
-    ["Minor Third"] = "Kleine Terz",
-    ["Perfect Fifth"] = "Reine Quinte",
-    ["Perfect Fourth"] = "Reine Quarte",
-    ["Perfect Octave"] = "Reine Oktave",
-    ["Perfect Unison"] = "Reine Prime",
-    ["Pitch"] = "Tonhöhe",
-    ["Plus Octaves"] = "Plus Oktaven",
-    ["Preserve Existing Notes"] = "Bestehende Noten beibehalten",
-    ["Simplify Spelling"] = "Notation vereinfachen",
-    ["Transposition Error"] = "Transpositionsfehler",
-    ["Up"] = "Hoch",
-    ["OK"] = "OK",
-    ["Cancel"] = "Abbrechen",
+    error_msg_transposition = "Finale kann einige der transponierten Tönhöhen nicht darstellen. Diese Tönhöhen wurden unverändert gelassen.",
+    augmented_fifth = "Übermäßige Quinte",
+    augmented_fourth = "Übermäßige Quarte",
+    augmented_second = "Übermäßige Sekunde",
+    augmented_seventh = "Übermäßige Septime",
+    augmented_sixth = "Übermäßige Sexte",
+    augmented_third = "Übermäßige Terz",
+    augmented_unison = "Übermäßige Prime",
+    diminished_fifth = "Verminderte Quinte",
+    diminished_fourth = "Verminderte Quarte",
+    diminished_octave = "Verminderte Oktave",
+    diminished_second = "Verminderte Sekunde",
+    diminished_seventh = "Verminderte Septime",
+    diminished_sixth = "Verminderte Sexte",
+    diminished_third = "Verminderte Terz",
+    direction = "Richtung",
+    down = "Runter",
+    interval = "Intervall",
+    major_second = "Große Sekunde",
+    major_seventh = "Große Septime",
+    major_sixth = "Große Sexte",
+    major_third = "Große Terz",
+    minor_second = "Kleine Sekunde",
+    minor_seventh = "Kleine Septime",
+    minor_sixth = "Kleine Sexte",
+    minor_third = "Kleine Terz",
+    perfect_fifth = "Reine Quinte",
+    perfect_fourth = "Reine Quarte",
+    perfect_octave = "Reine Oktave",
+    perfect_unison = "Reine Prime",
+    plus_octaves = "Plus Oktaven",
+    preserve_existing = "Bestehende Noten beibehalten",
+    simplify_spelling = "Notation vereinfachen",
+    transposition_error = "Transpositionsfehler",
+    up = "Hoch",
+    ok = "OK",
+    cancel = "Abbrechen",
 }
 
 return loc
diff --git a/src/localization/transpose_chromatic/es.lua b/src/localization/transpose_chromatic/es.lua
index 559c64ae..6c3c1bb7 100644
--- a/src/localization/transpose_chromatic/es.lua
+++ b/src/localization/transpose_chromatic/es.lua
@@ -2,45 +2,43 @@
 -- Localization es.lua for transpose_chromatic.lua
 --
 local loc = {
-    ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
-       "Finale no puede representar algunas de las notas traspuestas. Estas notas no se han cambiado.",
-    ["Augmented Fifth"] = "Quinta aumentada",
-    ["Augmented Fourth"] = "Cuarta aumentada",
-    ["Augmented Second"] = "Segunda aumentada",
-    ["Augmented Seventh"] = "Séptima aumentada",
-    ["Augmented Sixth"] = "Sexta aumentada",
-    ["Augmented Third"] = "Tercera aumentada",
-    ["Augmented Unison"] = "Unísono aumentado",
-    ["Diminished Fifth"] = "Quinta disminuida",
-    ["Diminished Fourth"] = "Cuarta disminuida",
-    ["Diminished Octave"] = "Octava disminuida",
-    ["Diminished Second"] = "Segunda disminuida",
-    ["Diminished Seventh"] = "Séptima disminuida",
-    ["Diminished Sixth"] = "Sexta disminuida",
-    ["Diminished Third"] = "Tercera disminuida",
-    ["Direction"] = "Dirección",
-    ["Down"] = "Abajo",
-    ["Interval"] = "Intervalo",
-    ["Major Second"] = "Segunda mayor",
-    ["Major Seventh"] = "Séptima mayor",
-    ["Major Sixth"] = "Sexta mayor",
-    ["Major Third"] = "Tercera mayor",
-    ["Minor Second"] = "Segunda menor",
-    ["Minor Seventh"] = "Séptima menor",
-    ["Minor Sixth"] = "Sexta menor",
-    ["Minor Third"] = "Tercera menor",
-    ["Perfect Fifth"] = "Quinta justa",
-    ["Perfect Fourth"] = "Cuarta justa",
-    ["Perfect Octave"] = "Octava justa",
-    ["Perfect Unison"] = "Unísono justo",
-    ["Pitch"] = "Tono",
-    ["Plus Octaves"] = "Más Octavas",
-    ["Preserve Existing Notes"] = "Preservar notas existentes",
-    ["Simplify Spelling"] = "Simplificar enarmonización",
-    ["Transposition Error"] = "Error de trasposición",
-    ["Up"] = "Arriba",
-    ["OK"] = "Aceptar",
-    ["Cancel"] = "Cancelar",
+    error_msg_transposition = "Finale no puede representar algunas de las notas traspuestas. Estas notas no se han cambiado.",
+    augmented_fifth = "Quinta aumentada",
+    augmented_fourth = "Cuarta aumentada",
+    augmented_second = "Segunda aumentada",
+    augmented_seventh = "Séptima aumentada",
+    augmented_sixth = "Sexta aumentada",
+    augmented_third = "Tercera aumentada",
+    augmented_unison = "Unísono aumentado",
+    diminished_fifth = "Quinta disminuida",
+    diminished_fourth = "Cuarta disminuida",
+    diminished_octave = "Octava disminuida",
+    diminished_second = "Segunda disminuida",
+    diminished_seventh = "Séptima disminuida",
+    diminished_sixth = "Sexta disminuida",
+    diminished_third = "Tercera disminuida",
+    direction = "Dirección",
+    down = "Abajo",
+    interval = "Intervalo",
+    major_second = "Segunda mayor",
+    major_seventh = "Séptima mayor",
+    major_sixth = "Sexta mayor",
+    major_third = "Tercera mayor",
+    minor_second = "Segunda menor",
+    minor_seventh = "Séptima menor",
+    minor_sixth = "Sexta menor",
+    minor_third = "Tercera menor",
+    perfect_fifth = "Quinta justa",
+    perfect_fourth = "Cuarta justa",
+    perfect_octave = "Octava justa",
+    perfect_unison = "Unísono justo",
+    plus_octaves = "Más Octavas",
+    preserve_existing = "Preservar notas existentes",
+    simplify_spelling = "Simplificar enarmonización",
+    transposition_error = "Error de trasposición",
+    up = "Arriba",
+    ok = "Aceptar",
+    cancel = "Cancelar",
 }
 
 return loc
diff --git a/src/transpose_by_step.lua b/src/transpose_by_step.lua
index 293629a3..ce5efbb4 100644
--- a/src/transpose_by_step.lua
+++ b/src/transpose_by_step.lua
@@ -1,6 +1,6 @@
 function plugindef(locale)
     local loc = {}
-    loc.en = {
+    loc.Base = {
         menu = "Transpose By Steps",
         desc = "Transpose by the number of steps given, simplifying the note spelling as needed."
     }
@@ -12,7 +12,7 @@ function plugindef(locale)
         menu = "Transponieren nach Schritten",
         desc = "Transponieren nach der angegebenen Anzahl von Schritten und vereinfachen die Notation nach Bedarf.",
     }
-    local t = locale and loc[locale:sub(1,2)] or loc.en
+    local t = locale and loc[locale:sub(1,2)] or loc.Base
     finaleplugin.RequireSelection = false
     finaleplugin.HandlesUndo = true -- not recognized by JW Lua or RGP Lua v0.55
     finaleplugin.Author = "Robert Patterson"
@@ -99,7 +99,7 @@ function create_dialog_box()
     dialog:CreateStatic(0, current_y + 2, "steps_label")
         :SetTextLocalized("Number Of Steps")
         :SetWidth(x_increment - 5)
-        :_FallbackCall("DoAutoResizeWidth", nil, true)
+        :_FallbackCall("DoAutoResizeWidth", nil)
     local edit_x = x_increment + utils.win_mac(0, 4)
     dialog:CreateEdit(edit_x, current_y, "num_steps")
         :SetText("")
@@ -107,10 +107,10 @@ function create_dialog_box()
     -- ok/cancel
     dialog:CreateOkButton()
         :SetTextLocalized("OK")
-        :_FallbackCall("DoAutoResizeWidth", nil, true)
+        :_FallbackCall("DoAutoResizeWidth", nil)
     dialog:CreateCancelButton()
         :SetTextLocalized("Cancel")
-        :_FallbackCall("DoAutoResizeWidth", nil, true)
+        :_FallbackCall("DoAutoResizeWidth", nil)
     -- registrations
     dialog:RegisterHandleOkButtonPressed(function(self)
         do_transpose_by_step(self:GetControl("num_steps"):GetInteger())
diff --git a/src/transpose_chromatic.lua b/src/transpose_chromatic.lua
index b16af8c2..9d41277e 100644
--- a/src/transpose_chromatic.lua
+++ b/src/transpose_chromatic.lua
@@ -1,6 +1,6 @@
 function plugindef(locale)
     local loc = {}
-    loc.en = {
+    loc.Base = {
         menu = "Transpose Chromatic",
         desc = "Chromatic transposition of selected region (supports microtone systems)."
     }
@@ -12,7 +12,7 @@ function plugindef(locale)
         menu = "Transponieren chromatisch",
         desc = "Chromatische Transposition des ausgewählten Abschnittes (unterstützt Mikrotonsysteme)."
     }
-    local t = locale and loc[locale:sub(1,2)] or loc.en
+    local t = locale and loc[locale:sub(1,2)] or loc.Base
     finaleplugin.RequireSelection = false
     finaleplugin.HandlesUndo = true -- not recognized by JW Lua or RGP Lua v0.55
     finaleplugin.Author = "Robert Patterson"
@@ -60,32 +60,32 @@ local loc = require("library.localization")
 local utils = require("library.utils")
 
 interval_names = interval_names or {
-    "Perfect Unison",
-    "Augmented Unison",
-    "Diminished Second",
-    "Minor Second",
-    "Major Second",
-    "Augmented Second",
-    "Diminished Third",
-    "Minor Third",
-    "Major Third",
-    "Augmented Third",
-    "Diminished Fourth",
-    "Perfect Fourth",
-    "Augmented Fourth",
-    "Diminished Fifth",
-    "Perfect Fifth",
-    "Augmented Fifth",
-    "Diminished Sixth",
-    "Minor Sixth",
-    "Major Sixth",
-    "Augmented Sixth",
-    "Diminished Seventh",
-    "Minor Seventh",
-    "Major Seventh",
-    "Augmented Seventh",
-    "Diminished Octave",
-    "Perfect Octave"
+    "perfect_unison",
+    "augmented_unison",
+    "diminished_second",
+    "minor_second",
+    "major_second",
+    "augmented_second",
+    "diminished_third",
+    "minor_third",
+    "major_third",
+    "augmented_third",
+    "diminished_fourth",
+    "perfect_fourth",
+    "augmented_fourth",
+    "diminished_fifth",
+    "perfect_fifth",
+    "augmented_fifth",
+    "diminished_sixth",
+    "minor_sixth",
+    "major_sixth",
+    "augmented_sixth",
+    "diminished_seventh",
+    "minor_seventh",
+    "major_seventh",
+    "augmented_seventh",
+    "diminished_octave",
+    "perfect_octave"
 }
 
 interval_disp_alts = interval_disp_alts or {
@@ -124,10 +124,7 @@ function do_transpose_chromatic(direction, interval_index, simplify, plus_octave
         finenv.StartNewUndoBlock(undostr, true) -- JW Lua automatically terminates the final undo block we start here
     end
     if not success then
-        global_dialog:CreateChildUI():AlertErrorLocalized(
-            "Finale is unable to represent some of the transposed pitches. These pitches were left unchanged.",
-            "Transposition Error"
-        )
+        global_dialog:CreateChildUI():AlertErrorLocalized("error_msg_transposition", "transposition_error")
     end
     return success
 end
@@ -140,40 +137,40 @@ function create_dialog_box()
     local x_increment = 85
     -- direction
     dialog:CreateStatic(0, current_y + 2, "direction_label")
-        :SetTextLocalized("Direction")
+        :SetTextLocalized("direction")
         :SetWidth(x_increment - 5)
-        :_FallbackCall("DoAutoResizeWidth", nil, true)
+        :_FallbackCall("DoAutoResizeWidth", nil)
     dialog:CreatePopup(x_increment, current_y, "direction_choice")
-        :AddStringsLocalized("Up", "Down"):SetWidth(x_increment)
+        :AddStringsLocalized("up", "down"):SetWidth(x_increment)
         :SetSelectedItem(0)
-        :_FallbackCall("DoAutoResizeWidth", nil, true)
+        :_FallbackCall("DoAutoResizeWidth", nil)
         :_FallbackCall("AssureNoHorizontalOverlap", nil, dialog:GetControl("direction_label"), 5)
     current_y = current_y + y_increment
     -- interval
     dialog:CreateStatic(0, current_y + 2, "interval_label")
-        :SetTextLocalized("Interval")
+        :SetTextLocalized("interval")
         :SetWidth(x_increment - 5)
-        :_FallbackCall("DoAutoResizeWidth", nil, true)
+        :_FallbackCall("DoAutoResizeWidth", nil)
     dialog:CreatePopup(x_increment, current_y, "interval_choice")
         :AddStringsLocalized(table.unpack(interval_names))
         :SetWidth(140)
         :SetSelectedItem(0)
-        :_FallbackCall("DoAutoResizeWidth", nil, true)
+        :_FallbackCall("DoAutoResizeWidth", nil)
         :_FallbackCall("AssureNoHorizontalOverlap", nil, dialog:GetControl("interval_label"), 5)
         :_FallbackCall("HorizontallyAlignLeftWith", nil, dialog:GetControl("direction_choice"))
     current_y = current_y + y_increment
     -- simplify checkbox
     dialog:CreateCheckbox(0, current_y + 2, "do_simplify")
-        :SetTextLocalized("Simplify Spelling")
+        :SetTextLocalized("simplify_spelling")
         :SetWidth(140)
         :SetCheck(0)
-        :_FallbackCall("DoAutoResizeWidth", nil, true)
+        :_FallbackCall("DoAutoResizeWidth", nil)
     current_y = current_y + y_increment
     -- plus octaves
     dialog:CreateStatic(0, current_y + 2, "plus_octaves_label")
-        :SetTextLocalized("Plus Octaves")
+        :SetTextLocalized("plus_octaves")
         :SetWidth(x_increment - 5)
-        :_FallbackCall("DoAutoResizeWidth", nil, true)
+        :_FallbackCall("DoAutoResizeWidth", nil)
     local edit_offset_x = utils.win_mac(0, 4)
     dialog:CreateEdit(x_increment + edit_offset_x, current_y, "plus_octaves")
         :SetText("")
@@ -182,18 +179,18 @@ function create_dialog_box()
     current_y = current_y + y_increment
     -- preserve existing notes
     dialog:CreateCheckbox(0, current_y + 2, "do_preserve")
-        :SetTextLocalized("Preserve Existing Notes")
+        :SetTextLocalized("preserve_existing")
         :SetWidth(140)
         :SetCheck(0)
-        :_FallbackCall("DoAutoResizeWidth", nil, true)
+        :_FallbackCall("DoAutoResizeWidth", nil)
     current_y = current_y + y_increment -- luacheck: ignore
     -- OK/Cxl
     dialog:CreateOkButton()
-        :SetTextLocalized("OK")
-        :_FallbackCall("DoAutoResizeWidth", nil, true)
+        :SetTextLocalized("ok")
+        :_FallbackCall("DoAutoResizeWidth", nil)
     dialog:CreateCancelButton()
-        :SetTextLocalized("Cancel")
-        :_FallbackCall("DoAutoResizeWidth", nil, true)
+        :SetTextLocalized("cancel")
+        :_FallbackCall("DoAutoResizeWidth", nil)
     -- registrations
     dialog:RegisterHandleOkButtonPressed(function(self)
             local direction = 1 -- up
diff --git a/src/transpose_enharmonic_down.lua b/src/transpose_enharmonic_down.lua
index c39c078e..613e18cd 100644
--- a/src/transpose_enharmonic_down.lua
+++ b/src/transpose_enharmonic_down.lua
@@ -1,6 +1,6 @@
 function plugindef(locale)
     local loc = {}
-    loc.en = {
+    loc.Base = {
         menu = "Enharmonic Transpose Down",
         desc = "Transpose down enharmonically all notes in the selected region."
     }
@@ -12,7 +12,7 @@ function plugindef(locale)
         menu = "Enharmonische Transposition nach unten",
         desc = "Transponieren alle Noten im ausgewählten Abschnitt enharmonisch nach unten.",
     }
-    local t = locale and loc[locale:sub(1,2)] or loc.en
+    local t = locale and loc[locale:sub(1,2)] or loc.Base
     finaleplugin.RequireSelection = true
     finaleplugin.Author = "Robert Patterson"
     finaleplugin.Copyright = "CC0 https://creativecommons.org/publicdomain/zero/1.0/"
diff --git a/src/transpose_enharmonic_up.lua b/src/transpose_enharmonic_up.lua
index 0f68dd75..7f244d27 100644
--- a/src/transpose_enharmonic_up.lua
+++ b/src/transpose_enharmonic_up.lua
@@ -1,6 +1,6 @@
 function plugindef(locale)
     local loc = {}
-    loc.en = {
+    loc.Base = {
         menu = "Enharmonic Transpose Up",
         desc = "Transpose up enharmonically all notes in the selected region."
     }
@@ -12,7 +12,7 @@ function plugindef(locale)
         menu = "Enharmonische Transposition nach oben",
         desc = "Transponieren alle Noten im ausgewählten Abschnitt enharmonisch nach oben.",
     }
-    local t = locale and loc[locale:sub(1,2)] or loc.en
+    local t = locale and loc[locale:sub(1,2)] or loc.Base
     finaleplugin.RequireSelection = true
     finaleplugin.Author = "Robert Patterson"
     finaleplugin.Copyright = "CC0 https://creativecommons.org/publicdomain/zero/1.0/"
diff --git a/utilities/localization_tool.lua b/utilities/localization_tool.lua
index e7e02e5f..0574dc35 100644
--- a/utilities/localization_tool.lua
+++ b/utilities/localization_tool.lua
@@ -179,8 +179,8 @@ as a localization.
 ]]
 local function create_localized_base_table_string(file_path)
     local t = create_localized_base_table(file_path)
-    local locale = mixin.UI():GetUserLocaleName()
-    local table_text = make_flat_table_string(file_path, locale:sub(1, 2), t)
+    local locale = "Base"
+    local table_text = make_flat_table_string(file_path, locale, t)
     global_contents[file_path] = table_text
     set_edit_text(table_text)
     -- finenv.UI():AlertInfo("localization_base table copied to clipboard", "")
@@ -448,11 +448,11 @@ local function on_plugindef(_control)
                 if not locale_exists then
                     plugindef_function[1] = "function plugindef(locale)"
                 end
-                local locale = mixin.UI():GetUserLocaleName()
+                local locale = "Base"
                 table.insert(plugindef_function, 2, tab_str .. "local loc = {}")
-                table.insert(plugindef_function, 3, tab_str .. "loc." .. locale:sub(1, 2) .. " = " .. base_strings)
+                table.insert(plugindef_function, 3, tab_str .. "loc." .. locale .. " = " .. base_strings)
                 table.insert(plugindef_function, 4,
-                    tab_str .. "local t = locale and loc[locale:sub(1, 2)] or loc." .. locale:sub(1, 2))
+                    tab_str .. "local t = locale and loc[locale:sub(1, 2)] or loc." .. locale)
             end
             mixin.UI():TextToClipboard(table.concat(plugindef_function, "\n") .. "\n")
             mixin.UI():AlertInfo("Localized plugindef function copied to clipboard.", "")
@@ -489,8 +489,7 @@ local function create_dialog()
         :AssureNoHorizontalOverlap(dlg:GetControl("open"), x_separator)
         :AddHandleCommand(on_popup)
     dlg:CreateComboBox(0, curr_y, "lang_list")
-        :SetWidth(0)
-        :DoAutoResizeWidth()
+        :DoAutoResizeWidth(0)
         :AddStrings(table.unpack(utils.create_keys_table(finale_supported_languages)))
         :SetText("Spanish")
     curr_y = curr_y + button_height

From 5118dcc23b113ce174ec15d4ee72606d40bfb40c Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Wed, 7 Feb 2024 13:30:47 -0600
Subject: [PATCH 53/61] Base localization added to all src scripts and
 utilities. Still need to work on format strings.

---
 src/localization/transpose_by_step/Base.lua         | 12 ++++++++++++
 src/localization/transpose_by_step/de.lua           | 11 +++++------
 src/localization/transpose_by_step/es.lua           | 11 +++++------
 src/localization/transpose_enharmonic_down/Base.lua |  9 +++++++++
 src/localization/transpose_enharmonic_down/de.lua   |  5 ++---
 src/localization/transpose_enharmonic_down/es.lua   |  5 ++---
 src/localization/transpose_enharmonic_up/Base.lua   |  9 +++++++++
 src/localization/transpose_enharmonic_up/de.lua     |  5 ++---
 src/localization/transpose_enharmonic_up/es.lua     |  5 ++---
 src/transpose_by_step.lua                           | 11 ++++-------
 src/transpose_enharmonic_down.lua                   |  8 ++------
 src/transpose_enharmonic_up.lua                     |  6 +-----
 12 files changed, 55 insertions(+), 42 deletions(-)
 create mode 100644 src/localization/transpose_by_step/Base.lua
 create mode 100644 src/localization/transpose_enharmonic_down/Base.lua
 create mode 100644 src/localization/transpose_enharmonic_up/Base.lua

diff --git a/src/localization/transpose_by_step/Base.lua b/src/localization/transpose_by_step/Base.lua
new file mode 100644
index 00000000..729ff391
--- /dev/null
+++ b/src/localization/transpose_by_step/Base.lua
@@ -0,0 +1,12 @@
+--
+-- Localization Base.lua for transpose_by_step.lua
+--
+local loc = {
+    error_msg_transposition = "Finale is unable to represent some of the transposed pitches. These pitches were left unchanged.",
+    number_of_steps = "Number Of Steps",
+    transposition_error = "Transposition Error",
+    ok = "OK",
+    cancel = "Cancel"
+}
+
+return loc
diff --git a/src/localization/transpose_by_step/de.lua b/src/localization/transpose_by_step/de.lua
index 1346f4c7..d2fb066c 100644
--- a/src/localization/transpose_by_step/de.lua
+++ b/src/localization/transpose_by_step/de.lua
@@ -2,12 +2,11 @@
 -- Localization de.lua for transpose_by_step.lua
 --
 local loc = {
-    ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
-        "Finale kann einige der transponierten Tönhöhen nicht darstellen. Diese Tönhöhen wurden unverändert gelassen.",
-    ["Number Of Steps"] = "Anzahl der Schritte",
-    ["Transposition Error"] = "Transpositionsfehler",
-    ["OK"] = "OK",
-    ["Cancel"] = "Abbrechen",
+    error_msg_transposition = "Finale kann einige der transponierten Tönhöhen nicht darstellen. Diese Tönhöhen wurden unverändert gelassen.",
+    number_of_steps = "Anzahl der Schritte",
+    transposition_error = "Transpositionsfehler",
+    ok = "OK",
+    cancel = "Abbrechen",
 }
 
 return loc
diff --git a/src/localization/transpose_by_step/es.lua b/src/localization/transpose_by_step/es.lua
index 27ffc260..3ab4eb97 100644
--- a/src/localization/transpose_by_step/es.lua
+++ b/src/localization/transpose_by_step/es.lua
@@ -2,12 +2,11 @@
 -- Localization es.lua for transpose_by_step.lua
 --
 local loc = {
-    ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
-        "Finale no puede representar algunas de las notas traspuestas. Estas notas no se han cambiado.",
-    ["Number Of Steps"] = "Número De Pasos",
-    ["Transposition Error"] = "Error de trasposición",
-    ["OK"] = "Aceptar",
-    ["Cancel"] = "Cancelar",
+    error_msg_transposition = "Finale no puede representar algunas de las notas traspuestas. Estas notas no se han cambiado.",
+    number_of_steps = "Número De Pasos",
+    transposition_error = "Error de trasposición",
+    ok = "Aceptar",
+    canel = "Cancelar"
 }
 
 return loc
diff --git a/src/localization/transpose_enharmonic_down/Base.lua b/src/localization/transpose_enharmonic_down/Base.lua
new file mode 100644
index 00000000..5d35feec
--- /dev/null
+++ b/src/localization/transpose_enharmonic_down/Base.lua
@@ -0,0 +1,9 @@
+--
+-- Localization Base.lua for transpose_enharmonic_down.lua
+--
+local loc = {
+    error_msg_transposition = "Finale is unable to represent some of the transposed pitches. These pitches were left unchanged.",
+    transposition_error = "Transposition Error"
+}
+
+return loc
diff --git a/src/localization/transpose_enharmonic_down/de.lua b/src/localization/transpose_enharmonic_down/de.lua
index 37bb90ef..19224f13 100644
--- a/src/localization/transpose_enharmonic_down/de.lua
+++ b/src/localization/transpose_enharmonic_down/de.lua
@@ -2,9 +2,8 @@
 -- Localization de.lua for transpose_enharmonic_down.lua
 --
 local loc = {
-    ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
-        "Finale kann einige der transponierten Tönhöhen nicht darstellen. Diese Tönhöhen wurden unverändert gelassen.",
-    ["Transposition Error"] = "Transpositionsfehler"
+    error_msg_transposition = "Finale kann einige der transponierten Tönhöhen nicht darstellen. Diese Tönhöhen wurden unverändert gelassen.",
+    transposition_error = "Transpositionsfehler"
 }
 
 return loc
diff --git a/src/localization/transpose_enharmonic_down/es.lua b/src/localization/transpose_enharmonic_down/es.lua
index 6fa8d0e0..7ceabf2f 100644
--- a/src/localization/transpose_enharmonic_down/es.lua
+++ b/src/localization/transpose_enharmonic_down/es.lua
@@ -2,9 +2,8 @@
 -- Localization es.lua for transpose_enharmonic_down.lua
 --
 local loc = {
-    ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
-        "Finale no puede representar algunas de las notas traspuestas. Estas notas no se han cambiado.",
-    ["Transposition Error"] = "Error de trasposición"
+    error_msg_transposition = "Finale no puede representar algunas de las notas traspuestas. Estas notas no se han cambiado.",
+    transposition_error = "Error de trasposición"
 }
 
 return loc
diff --git a/src/localization/transpose_enharmonic_up/Base.lua b/src/localization/transpose_enharmonic_up/Base.lua
new file mode 100644
index 00000000..fb832f01
--- /dev/null
+++ b/src/localization/transpose_enharmonic_up/Base.lua
@@ -0,0 +1,9 @@
+--
+-- Localization Base.lua for transpose_enharmonic_up.lua
+--
+local loc = {
+    error_msg_transposition = "Finale is unable to represent some of the transposed pitches. These pitches were left unchanged.",
+    transposition_error = "Transposition Error"
+}
+
+return loc
diff --git a/src/localization/transpose_enharmonic_up/de.lua b/src/localization/transpose_enharmonic_up/de.lua
index 96378b28..fa583027 100644
--- a/src/localization/transpose_enharmonic_up/de.lua
+++ b/src/localization/transpose_enharmonic_up/de.lua
@@ -2,9 +2,8 @@
 -- Localization de.lua for transpose_enharmonic_up.lua
 --
 local loc = {
-    ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
-        "Finale kann einige der transponierten Tönhöhen nicht darstellen. Diese Tönhöhen wurden unverändert gelassen.",
-    ["Transposition Error"] = "Transpositionsfehler"
+    error_msg_transposition = "Finale kann einige der transponierten Tönhöhen nicht darstellen. Diese Tönhöhen wurden unverändert gelassen.",
+    transposition_error = "Transpositionsfehler"
 }
 
 return loc
diff --git a/src/localization/transpose_enharmonic_up/es.lua b/src/localization/transpose_enharmonic_up/es.lua
index 364cb9f9..e56beb1c 100644
--- a/src/localization/transpose_enharmonic_up/es.lua
+++ b/src/localization/transpose_enharmonic_up/es.lua
@@ -2,9 +2,8 @@
 -- Localization es.lua for transpose_enharmonic_up.lua
 --
 local loc = {
-    ["Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."] =
-        "Finale no puede representar algunas de las notas traspuestas. Estas notas no se han cambiado.",
-    ["Transposition Error"] = "Error de trasposición"
+    error_msg_transposition = "Finale no puede representar algunas de las notas traspuestas. Estas notas no se han cambiado.",
+    transposition_error = "Error de trasposición"
 }
 
 return loc
diff --git a/src/transpose_by_step.lua b/src/transpose_by_step.lua
index ce5efbb4..56905aec 100644
--- a/src/transpose_by_step.lua
+++ b/src/transpose_by_step.lua
@@ -82,10 +82,7 @@ function do_transpose_by_step(global_number_of_steps_edit)
         finenv.StartNewUndoBlock(undostr, true) -- JW Lua automatically terminates the final undo block we start here
     end
     if not success then
-        global_dialog:CreateChildUI():AlertErrorLocalized(
-            "Finale is unable to represent some of the transposed pitches. These pitches were left unchanged.",
-            "Transposition Error"
-        )            
+        global_dialog:CreateChildUI():AlertErrorLocalized("error_msg_transposition", "transposition_error")
     end
     return success
 end
@@ -97,7 +94,7 @@ function create_dialog_box()
     local x_increment = 105
     -- number of steps
     dialog:CreateStatic(0, current_y + 2, "steps_label")
-        :SetTextLocalized("Number Of Steps")
+        :SetTextLocalized("number_of_steps")
         :SetWidth(x_increment - 5)
         :_FallbackCall("DoAutoResizeWidth", nil)
     local edit_x = x_increment + utils.win_mac(0, 4)
@@ -106,10 +103,10 @@ function create_dialog_box()
         :_FallbackCall("AssureNoHorizontalOverlap", nil, dialog:GetControl("steps_label"), 5)
     -- ok/cancel
     dialog:CreateOkButton()
-        :SetTextLocalized("OK")
+        :SetTextLocalized("ok")
         :_FallbackCall("DoAutoResizeWidth", nil)
     dialog:CreateCancelButton()
-        :SetTextLocalized("Cancel")
+        :SetTextLocalized("cancel")
         :_FallbackCall("DoAutoResizeWidth", nil)
     -- registrations
     dialog:RegisterHandleOkButtonPressed(function(self)
diff --git a/src/transpose_enharmonic_down.lua b/src/transpose_enharmonic_down.lua
index 613e18cd..86c6b800 100644
--- a/src/transpose_enharmonic_down.lua
+++ b/src/transpose_enharmonic_down.lua
@@ -44,7 +44,7 @@ function plugindef(locale)
 end
 
 local transposition = require("library.transposition")
-local loc = require('library.localization')
+local loc = require("library.localization")
 
 function transpose_enharmonic_down()
     local success = true
@@ -54,11 +54,7 @@ function transpose_enharmonic_down()
         end
     end
     if not success then
-        finenv.UI():AlertError(
-            loc.localize(
-                "Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."),
-            loc.localize("Transposition Error")
-        )
+        finenv.UI():AlertError(loc.localize("error_msg_transposition"), loc.localize("transposition_error"))
     end
 end
 
diff --git a/src/transpose_enharmonic_up.lua b/src/transpose_enharmonic_up.lua
index 7f244d27..ca68a99e 100644
--- a/src/transpose_enharmonic_up.lua
+++ b/src/transpose_enharmonic_up.lua
@@ -54,11 +54,7 @@ function transpose_enharmonic_up()
         end
     end
     if not success then
-        finenv.UI():AlertError(
-            loc.localize(
-                "Finale is unable to represent some of the transposed pitches. These pitches were left unchanged."),
-            loc.localize("Transposition Error")
-        )
+        finenv.UI():AlertError(loc.localize("error_msg_transposition"), loc.localize("transposition_error"))
     end
 end
 

From b38d2ebfe8a284c7a6c7a2c2810eadb881008723 Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Wed, 7 Feb 2024 13:56:29 -0600
Subject: [PATCH 54/61] auto_layout.lua code complete

---
 samples/auto_layout.lua | 306 ++++++++++++++++++++--------------------
 1 file changed, 156 insertions(+), 150 deletions(-)

diff --git a/samples/auto_layout.lua b/samples/auto_layout.lua
index dbdda65a..fced12f8 100644
--- a/samples/auto_layout.lua
+++ b/samples/auto_layout.lua
@@ -18,178 +18,178 @@ local localization = require('library.localization')
 -- This table was auto-generated with `utilities/localization_tool.lua`
 -- Then it was edited to include only the strings that need to be localized.
 --
-localization.en = -- this is en_GB due to spelling of "Localisation"
+localization.Base = -- this is en_GB due to spelling of "Localisation"
 {
-    ["Action Button"] = "Action Button",
-    ["Choices"] = "Choices",
-    ["Close"] = "Close",
-    ["First Option"] = "First Option",
-    ["Fourth Option"] = "Fourth Option",
-    ["Left Checkbox Option 1"] = "Left Checkbox Option 1",
-    ["Left Checkbox Option 2"] = "Left Checkbox Option 2",
-    ["Menu"] = "Menu",
-    ["Right Three-State Option"] = "Right Three-State Option",
-    ["Second Option"] = "Second Option",
-    ["Short "] = "Short ",
-    ["Test Autolayout With Localisation"] = "Test Autolayout With Localisation",
-    ["Third Option"] = "Third Option",
-    ["This is long menu text "] = "This is long menu text ",
-    ["This is long text choice "] = "This is long text choice ",
-    ["This is longer option text "] = "This is longer option text ",
+    action_button = "Action Button",
+    choices = "Choices",
+    close = "Close",
+    first_option = "First Option",
+    fourth_option = "Fourth Option",
+    left_checkbox1 = "Left Checkbox Option 1",
+    left_checkbox2 = "Left Checkbox Option 2",
+    menu = "Menu",
+    right_three_state = "Right Three-State Option",
+    second_option = "Second Option",
+    short = "Short %d",
+    test_autolayout = "Test Autolayout With Localisation",
+    third_option = "Third Option",
+    long_menu_text = "This is long menu text %d",
+    long_text_choice = "This is long text choice %d",
+    longer_option_text = "This is longer option text %d",
 }
 
 localization.en_US =
 {
-    ["Test Autolayout With Localisation"] = "Test Autolayout With Localization",
+    test_autolayout = "Test Autolayout With Localization"
 }
 
 --
 -- The rest of the localization tables were created one-at-a-time with the `utilities/localization_tool.lua` script.
 --
 localization.es = {
-    ["Action Button"] = "Botón de Acción",
-    ["Choices"] = "Opciones",
-    ["Close"] = "Cerrar",
-    ["First Option"] = "Primera Opción",
-    ["Fourth Option"] = "Cuarta Opción",
-    ["Left Checkbox Option 1"] = "Opción de Casilla de Verificación Izquierda 1",
-    ["Left Checkbox Option 2"] = "Opción de Casilla de Verificación Izquierda 2",
-    ["Menu"] = "Menú",
-    ["Right Three-State Option"] = "Opción de Tres Estados a la Derecha",
-    ["Second Option"] = "Segunda Opción",
-    ["Short "] = "Corto ",
-    ["Test Autolayout With Localisation"] = "Prueba de Autodiseño con Localización",
-    ["Third Option"] = "Tercera Opción",
-    ["This is long menu text "] = "Este es un texto de menú largo ",
-    ["This is long text choice "] = "Esta es una elección de texto largo ",
-    ["This is longer option text "] = "Este es un texto de opción más largo ",
+    action_button = "Botón de Acción",
+    choices = "Opciones",
+    close = "Cerrar",
+    first_option = "Primera Opción",
+    fourth_option = "Cuarta Opción",
+    left_checkbox1 = "Opción de Casilla de Verificación Izquierda 1",
+    left_checkbox2 = "Opción de Casilla de Verificación Izquierda 2",
+    menu = "Menú",
+    right_three_state = "Opción de Tres Estados a la Derecha",
+    second_option = "Segunda Opción",
+    short = "Corto %d",
+    test_autolayout = "Prueba de Autodiseño con Localización",
+    third_option = "Tercera Opción",
+    long_menu_text = "Este es un texto de menú largo %d",
+    long_text_choice = "Esta es una elección de texto largo %d",
+    longer_option_text = "Este es un texto de opción más largo %d",
 }
 
 --
 -- This table was auto-generated with `utilities/localization_tool.lua`
 --
-localization.jp = {
-    ["Action Button"] = "アクションボタン",
-    ["Choices"] = "選択肢",
-    ["Close"] = "閉じる",
-    ["First Option"] = "最初のオプション",
-    ["Fourth Option"] = "第四のオプション",
-    ["Left Checkbox Option 1"] = "左チェックボックスオプション1",
-    ["Left Checkbox Option 2"] = "左チェックボックスオプション2",
-    ["Menu"] = "メニュー",
-    ["Right Three-State Option"] = "右三状態オプション",
-    ["Second Option"] = "第二のオプション",
-    ["Short "] = "短い ",
-    ["Test Autolayout With Localisation"] = "ローカリゼーションでのオートレイアウトのテスト",
-    ["Third Option"] = "第三のオプション",
-    ["This is long menu text "] = "これは長いメニューテキストです ",
-    ["This is long text choice "] = "これは長いテキスト選択です ",
-    ["This is longer option text "] = "これはより長いオプションテキストです ",
+localization.ja = {
+    action_button = "アクションボタン",
+    choices = "選択肢",
+    close = "閉じる",
+    first_option = "最初のオプション",
+    fourth_option = "第四のオプション",
+    left_checkbox1 = "左チェックボックスオプション1",
+    left_checkbox2 = "左チェックボックスオプション2",
+    menu = "メニュー",
+    right_three_state = "右三状態オプション",
+    second_option = "第二のオプション",
+    short = "短い %d",
+    test_autolayout = "ローカリゼーションでのオートレイアウトのテスト",
+    third_option = "第三のオプション",
+    long_menu_text = "これは第%d長いメニューテキストです",
+    long_text_choice = "これは第%d長いテキストの選択です",
+    longer_option_text = "これは第%dより長いオプションテキストです ",
 }
 
 --
 -- This table was auto-generated with `utilities/localization_tool.lua`
 --
 localization.de = {
-    ["Action Button"] = "Aktionsknopf",
-    ["Choices"] = "Auswahlmöglichkeiten",
-    ["Close"] = "Schließen",
-    ["First Option"] = "Erste Option",
-    ["Fourth Option"] = "Vierte Option",
-    ["Left Checkbox Option 1"] = "Linke Checkbox Option 1",
-    ["Left Checkbox Option 2"] = "Linke Checkbox Option 2",
-    ["Menu"] = "Menü",
-    ["Right Three-State Option"] = "Rechte Dreizustandsoption",
-    ["Second Option"] = "Zweite Option",
-    ["Short "] = "Kurz ",
-    ["Test Autolayout With Localisation"] = "Test von Autolayout mit Lokalisierung",
-    ["Third Option"] = "Dritte Option",
-    ["This is long menu text "] = "Dies ist ein langer Menütext ",
-    ["This is long text choice "] = "Dies ist eine lange Textauswahl ",
-    ["This is longer option text "] = "Dies ist ein längerer Optionstext ",
+    action_button = "Aktionsknopf",
+    choices = "Auswahlmöglichkeiten",
+    close = "Schließen",
+    first_option = "Erste Option",
+    fourth_option = "Vierte Option",
+    left_checkbox1 = "Linke Checkbox Option 1",
+    left_checkbox2 = "Linke Checkbox Option 2",
+    menu = "Menü",
+    right_three_state = "Rechte Dreizustandsoption",
+    second_option = "Zweite Option",
+    short = "Kurz %d",
+    test_autolayout = "Test von Autolayout mit Lokalisierung",
+    third_option = "Dritte Option",
+    long_menu_text = "Dies ist ein langer Menütext %d",
+    long_text_choice = "Dies ist eine lange Textauswahl %d",
+    longer_option_text = "Dies ist ein längerer Optionstext %d",
 }
 
 localization.fr = {
-    ["Action Button"] = "Bouton d'action",
-    ["Choices"] = "Choix",
-    ["Close"] = "Close",
-    ["First Option"] = "Première Option",
-    ["Fourth Option"] = "Quatrième Option",
-    ["Left Checkbox Option 1"] = "Option de case à cocher gauche 1",
-    ["Left Checkbox Option 2"] = "Option de case à cocher gauche 2",
-    ["Menu"] = "Menu",
-    ["Right Three-State Option"] = "Option à trois états à droite",
-    ["Second Option"] = "Deuxième Option",
-    ["Short "] = "Court ",
-    ["Test Autolayout With Localisation"] = "Test de AutoLayout avec Localisation",
-    ["Third Option"] = "Troisième Option",
-    ["This is long menu text "] = "Ceci est un long texte de menu ",
-    ["This is long text choice "] = "Ceci est un long choix de texte ",
-    ["This is longer option text "] = "Ceci est un texte d'option plus long ",
+    action_button = "Bouton d'action",
+    choices = "Choix",
+    close = "Close",
+    first_option = "Première Option",
+    fourth_option = "Quatrième Option",
+    left_checkbox1 = "Option de case à cocher gauche 1",
+    left_checkbox2 = "Option de case à cocher gauche 2",
+    menu = "Menu",
+    right_three_state = "Option à trois états à droite",
+    second_option = "Deuxième Option",
+    short = "Court %d",
+    test_autolayout = "Test de AutoLayout avec Localisation",
+    third_option = "Troisième Option",
+    long_menu_text = "Ceci est un long texte de menu %d",
+    long_text_choice = "Ceci est un long choix de texte %d",
+    longer_option_text = "Ceci est un texte d'option plus long %d",
 }
 
 localization.zh = {
-    ["Action Button"] = "操作按钮",
-    ["Choices"] = "选择:",
-    ["Close"] = "关闭",
-    ["First Option"] = "第一选项:",
-    ["Fourth Option"] = "第四选项:",
-    ["Left Checkbox Option 1"] = "左侧复选框选项1",
-    ["Left Checkbox Option 2"] = "左侧复选框选项2",
-    ["Menu"] = "菜单:",
-    ["Right Three-State Option"] = "右侧三态选项",
-    ["Second Option"] = "第二选项:",
-    ["Short "] = "短 ",
-    ["Test Autolayout With Localisation"] = "自动布局与本地化测试",
-    ["Third Option"] = "第三选项:",
-    ["This is long menu text "] = "这是长菜单文本 ",
-    ["This is long text choice "] = "这是长文本选择 ",
-    ["This is longer option text "] = "这是更长的选项文本 ",
+    action_button = "操作按钮",
+    choices = "选择:",
+    close = "关闭",
+    first_option = "第一选项:",
+    fourth_option = "第四选项:",
+    left_checkbox1 = "左侧复选框选项1",
+    left_checkbox2 = "左侧复选框选项2",
+    menu = "菜单:",
+    right_three_state = "右侧三态选项",
+    second_option = "第二选项:",
+    short = "短 %d",
+    test_autolayout = "自动布局与本地化测试",
+    third_option = "第三选项:",
+    long_menu_text = "这是长菜单文本 %d",
+    long_text_choice = "这是长文本选择 %d",
+    longer_option_text = "这是更长的选项文本 %d",
 }
 
 localization.ar = {
-    ["Action Button"] = "زر العمل",
-    ["Choices"] = "الخيارات",
-    ["Close"] = "إغلاق",
-    ["First Option"] = "الخيار الأول",
-    ["Fourth Option"] = "الخيار الرابع",
-    ["Left Checkbox Option 1"] = "خيار المربع الأول على اليسار",
-    ["Left Checkbox Option 2"] = "خيار المربع الثاني على اليسار",
-    ["Menu"] = "القائمة",
-    ["Right Three-State Option"] = "خيار الحالة الثلاثية اليمين",
-    ["Second Option"] = "الخيار الثاني",
-    ["Short "] = "قصير ",
-    ["Test Autolayout With Localisation"] = "اختبار التخطيط التلقائي مع التعريب",
-    ["Third Option"] = "الخيار الثالث",
-    ["This is long menu text "] = "هذا نص قائمة طويل ",
-    ["This is long text choice "] = "هذا خيار نص طويل ",
-    ["This is longer option text "] = "هذا نص خيار أطول ",
+    action_button = "زر العمل",
+    choices = "الخيارات",
+    close = "إغلاق",
+    first_option = "الخيار الأول",
+    fourth_option = "الخيار الرابع",
+    left_checkbox1 = "خيار المربع الأول على اليسار",
+    left_checkbox2 = "خيار المربع الثاني على اليسار",
+    menu = "القائمة",
+    right_three_state = "خيار الحالة الثلاثية اليمين",
+    second_option = "الخيار الثاني",
+    short = "قصير %d",
+    test_autolayout = "اختبار التخطيط التلقائي مع التعريب",
+    third_option = "الخيار الثالث",
+    long_menu_text = "هذا نص قائمة طويل %d",
+    long_text_choice = "هذا خيار نص طويل %d",
+    longer_option_text = "هذا نص خيار أطول %d",
 }
 
 localization.fa = {
-    ["Action Button"] = "دکمه عملیات",
-    ["Choices"] = "گزینه ها",
-    ["Close"] = "بستن",
-    ["First Option"] = "گزینه اول",
-    ["Fourth Option"] = "گزینه چهارم",
-    ["Left Checkbox Option 1"] = "گزینه چک باکس سمت چپ 1",
-    ["Left Checkbox Option 2"] = "گزینه چک باکس سمت چپ 2",
-    ["Menu"] = "منو",
-    ["Right Three-State Option"] = "گزینه سه حالته سمت راست",
-    ["Second Option"] = "گزینه دوم",
-    ["Short "] = "کوتاه ",
-    ["Test Autolayout With Localisation"] = "تست آتولایوت با بومی سازی",
-    ["Third Option"] = "گزینه سوم",
-    ["This is long menu text "] = "این متن منوی طولانی است ",
-    ["This is long text choice "] = "این یک انتخاب متن طولانی است ",
-    ["This is longer option text "] = "این متن گزینه طولانی تر است ",
+    action_button = "دکمه عملیات",
+    choices = "گزینه ها",
+    close = "بستن",
+    first_option = "گزینه اول",
+    fourth_option = "گزینه چهارم",
+    left_checkbox1 = "گزینه چک باکس سمت چپ 1",
+    left_checkbox2 = "گزینه چک باکس سمت چپ 2",
+    menu = "منو",
+    right_three_state = "گزینه سه حالته سمت راست",
+    second_option = "گزینه دوم",
+    short = "کوتاه %d",
+    test_autolayout = "تست آتولایوت با بومی سازی",
+    third_option = "گزینه سوم",
+    long_menu_text = "این متن منوی طولانی است %d",
+    long_text_choice = "این یک انتخاب متن طولانی است %d",
+    longer_option_text = "این متن گزینه طولانی تر است %d",
 }
 
-localization.set_locale("fa")
+localization.set_locale("es")
 
 function create_dialog()
     local dlg = mixin.FCXCustomLuaWindow()
-    dlg:SetTitleLocalized("Test Autolayout With Localisation")
+    dlg:SetTitleLocalized("test_autolayout")
 
     local line_no = 0
     local y_increment = 22
@@ -199,7 +199,7 @@ function create_dialog()
     -- left side
     dlg:CreateStatic(0, line_no * y_increment, "option1-label")
         :DoAutoResizeWidth(0)
-        :SetTextLocalized("First Option")
+        :SetTextLocalized("first_option")
     dlg:CreateEdit(0, line_no * y_increment - utils.win_mac(2, 3), "option1")
         :SetInteger(1)
         :AssureNoHorizontalOverlap(dlg:GetControl("option1-label"), label_edit_separ)
@@ -207,12 +207,12 @@ function create_dialog()
 
     dlg:CreateCheckbox(0, line_no * y_increment, "left-checkbox1")
         :DoAutoResizeWidth(0)
-        :SetTextLocalized("Left Checkbox Option 1")
+        :SetTextLocalized("left_checkbox1")
     line_no = line_no + 1
 
     dlg:CreateStatic(0, line_no * y_increment, "option2-label")
         :DoAutoResizeWidth(0)
-        :SetTextLocalized("Second Option")
+        :SetTextLocalized("second_option")
     dlg:CreateEdit(10, line_no * y_increment - utils.win_mac(2, 3), "option2")
         :SetInteger(2)
         :AssureNoHorizontalOverlap(dlg:GetControl("option2-label"), label_edit_separ)
@@ -221,7 +221,7 @@ function create_dialog()
 
     dlg:CreateCheckbox(0, line_no * y_increment, "left-checkbox2")
         :DoAutoResizeWidth(0)
-        :SetTextLocalized("Left Checkbox Option 2")
+        :SetTextLocalized("left_checkbox2")
     line_no = line_no + 1
 
     -- center vertical line
@@ -235,7 +235,7 @@ function create_dialog()
     -- right side
     dlg:CreateStatic(0, line_no * y_increment, "option3-label")
         :DoAutoResizeWidth(0)
-        :SetTextLocalized("Third Option")
+        :SetTextLocalized("third_option")
         :AssureNoHorizontalOverlap(vertical_line, center_padding)
     dlg:CreateEdit(0, line_no * y_increment - utils.win_mac(2, 3), "option3")
         :SetInteger(3)
@@ -244,14 +244,14 @@ function create_dialog()
 
     dlg:CreateCheckbox(0, line_no * y_increment, "right-checkbox1")
         :DoAutoResizeWidth(0)
-        :SetTextLocalized("Right Three-State Option")
+        :SetTextLocalized("right_three_state")
         :SetThreeStatesMode(true)
         :AssureNoHorizontalOverlap(vertical_line, center_padding)
     line_no = line_no + 1
 
     dlg:CreateStatic(0, line_no * y_increment, "option4-label")
         :DoAutoResizeWidth(0)
-        :SetTextLocalized("Fourth Option")
+        :SetTextLocalized("fourth_option")
         :AssureNoHorizontalOverlap(vertical_line, center_padding)
     dlg:CreateEdit(0, line_no * y_increment - utils.win_mac(2, 3), "option4")
         :SetInteger(4)
@@ -261,7 +261,7 @@ function create_dialog()
 
     dlg:CreateButton(0, line_no * y_increment)
         :DoAutoResizeWidth()
-        :SetTextLocalized("Action Button")
+        :SetTextLocalized("action_button")
         :AssureNoHorizontalOverlap(vertical_line, center_padding)
         :HorizontallyAlignRightWith(dlg:GetControl("option4"))
 --        :HorizontallyAlignRightWithFurthest()
@@ -276,33 +276,37 @@ function create_dialog()
     local start_line_no = line_no
     dlg:CreateStatic(0, line_no * y_increment, "popup_label")
         :DoAutoResizeWidth(0)
-        :SetTextLocalized("Menu")
+        :SetTextLocalized("menu")
     local ctrl_popup = dlg:CreatePopup(0, line_no * y_increment - utils.win_mac(2, 2), "popup")
         :DoAutoResizeWidth(0)
         :AssureNoHorizontalOverlap(dlg:GetControl("popup_label"), label_edit_separ)
     for counter = 1, 3 do
+        local format_string
         if counter == 3 then
-            ctrl_popup:AddStringLocalized(localization.localize("This is long menu text ") .. counter)
+            format_string = localization.localize("long_menu_text")
         else
-            ctrl_popup:AddStringLocalized(localization.localize("Short ") .. counter)
+            format_string = localization.localize("short")
         end
+        ctrl_popup:AddString(string.format(format_string, counter))
     end
     ctrl_popup:SetSelectedItem(0)
     line_no = line_no + 1
 
     dlg:CreateStatic(0, line_no * y_increment, "cbobox_label")
         :DoAutoResizeWidth(0)
-        :SetTextLocalized("Choices")
+        :SetTextLocalized("choices")
     local ctrl_cbobox = dlg:CreateComboBox(0, line_no * y_increment - utils.win_mac(2, 3), "cbobox")
         :DoAutoResizeWidth(40)
         :AssureNoHorizontalOverlap(dlg:GetControl("cbobox_label"), label_edit_separ)
         :HorizontallyAlignLeftWith(ctrl_popup)
     for counter = 1, 3 do
+        local format_string
         if counter == 3 then
-            ctrl_cbobox:AddString(localization.localize("This is long text choice ") .. counter)
+            format_string = localization.localize("long_text_choice")
         else
-            ctrl_cbobox:AddString(localization.localize("Short ") .. counter)
+            format_string = localization.localize("short")
         end
+        ctrl_cbobox:AddString(string.format(format_string, counter))
     end
     ctrl_cbobox:SetSelectedItem(0)
     line_no = line_no + 1 -- luacheck: ignore
@@ -314,17 +318,19 @@ function create_dialog()
         rbtn:DoAutoResizeWidth(0)
             :AssureNoHorizontalOverlap(ctrl_popup, 10)
             :AssureNoHorizontalOverlap(ctrl_cbobox, 10)
+        local format_string
         if counter == 2 then
-            rbtn:SetTextLocalized(localization.localize("This is longer option text ") .. counter)
+            format_string = localization.localize("longer_option_text")
         else
-            rbtn:SetTextLocalized(localization.localize("Short ") .. counter)
+            format_string = localization.localize("short")
         end
+        rbtn:SetText(string.format(format_string, counter))
         counter = counter + 1
     end
     line_no = line_no + 2
 
     dlg:CreateCloseButton(0, line_no * y_increment + 5)
-        :SetTextLocalized("Close")
+        :SetTextLocalized("close")
         :DoAutoResizeWidth()
         :HorizontallyAlignRightWithFurthest()
 

From c4c495dd2657832f85f7fec584bec31a6fac0c95 Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Thu, 8 Feb 2024 14:07:58 -0600
Subject: [PATCH 55/61] update with latest agreed changes

---
 samples/auto_layout.lua                       |  2 +-
 src/baseline_move_reset.lua                   |  4 +-
 src/library/localization.lua                  | 68 ++++++++++++++-----
 .../transpose_by_step/{Base.lua => en.lua}    |  2 +-
 .../transpose_chromatic/{Base.lua => en.lua}  |  2 +-
 .../en.lua}                                   |  2 +-
 .../en.lua}                                   |  2 +-
 src/transpose_by_step.lua                     |  4 +-
 src/transpose_chromatic.lua                   |  4 +-
 src/transpose_enharmonic_down.lua             |  4 +-
 src/transpose_enharmonic_up.lua               |  4 +-
 utilities/localization_tool.lua               | 22 +++++-
 12 files changed, 84 insertions(+), 36 deletions(-)
 rename src/localization/transpose_by_step/{Base.lua => en.lua} (85%)
 rename src/localization/transpose_chromatic/{Base.lua => en.lua} (96%)
 rename src/localization/{transpose_enharmonic_up/Base.lua => transpose_enharmonic_down/en.lua} (78%)
 rename src/localization/{transpose_enharmonic_down/Base.lua => transpose_enharmonic_up/en.lua} (78%)

diff --git a/samples/auto_layout.lua b/samples/auto_layout.lua
index fced12f8..c6fbc472 100644
--- a/samples/auto_layout.lua
+++ b/samples/auto_layout.lua
@@ -18,7 +18,7 @@ local localization = require('library.localization')
 -- This table was auto-generated with `utilities/localization_tool.lua`
 -- Then it was edited to include only the strings that need to be localized.
 --
-localization.Base = -- this is en_GB due to spelling of "Localisation"
+localization.en = -- this is en_GB due to spelling of "Localisation"
 {
     action_button = "Action Button",
     choices = "Choices",
diff --git a/src/baseline_move_reset.lua b/src/baseline_move_reset.lua
index 4d6b51a1..a2a1d169 100644
--- a/src/baseline_move_reset.lua
+++ b/src/baseline_move_reset.lua
@@ -1,6 +1,6 @@
 function plugindef(locale)
     local loc = {}
-    loc.Base = {
+    loc.en = {
         addl_menus = [[
             Move Lyric Baselines Up
             Reset Lyric Baselines
@@ -72,7 +72,7 @@ function plugindef(locale)
         menu = "Mover las líneas de referencia de las letras hacia abajo",
         desc = "Mueve todas las líneas de referencia de las letras un espacio hacia abajo en los sistemas de pentagramas seleccionadas",
     }
-    local t = locale and loc[locale:sub(1, 2)] or loc.Base
+    local t = locale and loc[locale:sub(1, 2)] or loc.en
     finaleplugin.RequireSelection = true
     finaleplugin.Author = "Robert Patterson"
     finaleplugin.Version = "1.1"
diff --git a/src/library/localization.lua b/src/library/localization.lua
index 6c6729e9..dc9333a1 100644
--- a/src/library/localization.lua
+++ b/src/library/localization.lua
@@ -2,13 +2,13 @@
 $module Localization
 
 This library provides localization services to scripts. Note that this library cannot be used inside
-a `plugindef` function, because the Lua plugin does not load any dependencies when it calls `plugindef`.
+a `plugindef` function, because the Lua plugin for Finale does not load any dependencies when it calls `plugindef`.
 
 **Executive Summary**
 
 - Create language tables containing each user-facing string as a value with a key. The key can be any string value.
 - Save the language tables in the `localization` subdirectory as shown below.
-- Use the `...Localized` methods with `mixin` or if not using `mixin`, require the `localization`
+- Use the `*Localized` methods with `mixin` or if not using `mixin`, require the `localization`
 library directly and wrap any user-facing string in a call to `localization.localize`.
 
 **Details**
@@ -23,8 +23,8 @@ src/
     my_highly_useful_script.lua
     localization/
         my_highly_useful_script/
-            Base.lua
             de.lua
+            en.lua
             es.lua
             es_ES.lua
             jp.lua
@@ -34,11 +34,11 @@ src/
 
 Each localization lua should return a table of keys and translations.
 
-Base:
+English:
 
 ```
 --
--- Base.lua:
+-- en.lua:
 --
 local t = {
     hello = "Hello",
@@ -91,10 +91,10 @@ local t = {
 return t
 ```
 
-The keys do not have to be user-friendly strings, but they should be the same in all tables. The recommended
-approach is to provide a `Base.lua` table that contains fallback translations if no others or available.
-For example, if you want your fallback language to be English, provide `Base.lua` with English translations rather
-than providing `en.lua`. Any time you wish to add another language, you simply add it to the subfolder for the script,
+The keys do not have to be user-friendly strings, but they should be the same in all tables. The default
+fallback language is `en.lua` (English). These will be used if no languges exists that matches the user's
+preferences. You can override this default with a different language by calling `set_fallback_locale`.
+Any time you wish to add another language, you simply add it to the subfolder for the script,
 and no further action is required.
 
 The `mixin` library provides automatic localization with the `...Localized` methods. Localized versions of user-facing
@@ -122,6 +122,8 @@ local locale = (function()
         return "en_US"
     end)()
 
+local fallback_locale = "en"
+
 local script_name = library.calc_script_name()
 
 --[[
@@ -151,6 +153,29 @@ function localization.get_locale()
     return locale
 end
 
+--[[
+% set_fallback_locale
+
+Sets the fallback locale to a specified value. This value is used when no locale exists that matches the user's
+set locale. The default is "en".
+
+@ input_locale (string) the 2-letter lowercase language code or 5-character regional locale code
+]]
+function localization.set_fallback_locale(input_locale)
+    fallback_locale = input_locale:gsub("-", "_")
+end
+
+--[[
+% get_fallback_locale
+
+Returns the fallback locale value that the localization library is using. See `set_fallback_locale` for more information.
+
+: (string) the current fallback locale string that the localization library is using
+]]
+function localization.get_fallback_locale()
+    return fallback_locale
+end
+
 -- This function finds a localization string table if it exists or requires it if it doesn't.
 local function get_localized_table(try_locale)
     if type(localization[try_locale]) == "table" then
@@ -167,6 +192,20 @@ local function get_localized_table(try_locale)
     return localization[try_locale]
 end
 
+local function try_locale_or_language(try_locale)
+    local t = get_localized_table(try_locale)
+    if t then
+        return t
+    end
+    if #try_locale > 2 then
+        t = get_localized_table(try_locale:sub(1, 2))
+        if t then
+            return t
+        end
+    end
+    return nil
+end
+
 --[[
 % localize
 
@@ -183,19 +222,12 @@ function localization.localize(input_string)
     end
     assert(type(locale) == "string", "invalid locale setting " .. tostring(locale))
     
-    local t = get_localized_table(locale)
+    local t = try_locale_or_language(locale)
     if t and t[input_string] then
         return t[input_string]
     end
 
-    if #locale > 2 then
-        t = get_localized_table(locale:sub(1, 2))
-        if t and t[input_string] then
-            return t[input_string]
-        end
-    end
-
-    t = get_localized_table("Base")
+    t = get_localized_table(fallback_locale)
     
     return t and t[input_string] or input_string
 end
diff --git a/src/localization/transpose_by_step/Base.lua b/src/localization/transpose_by_step/en.lua
similarity index 85%
rename from src/localization/transpose_by_step/Base.lua
rename to src/localization/transpose_by_step/en.lua
index 729ff391..3be901e0 100644
--- a/src/localization/transpose_by_step/Base.lua
+++ b/src/localization/transpose_by_step/en.lua
@@ -1,5 +1,5 @@
 --
--- Localization Base.lua for transpose_by_step.lua
+-- Localization en.lua for transpose_by_step.lua
 --
 local loc = {
     error_msg_transposition = "Finale is unable to represent some of the transposed pitches. These pitches were left unchanged.",
diff --git a/src/localization/transpose_chromatic/Base.lua b/src/localization/transpose_chromatic/en.lua
similarity index 96%
rename from src/localization/transpose_chromatic/Base.lua
rename to src/localization/transpose_chromatic/en.lua
index ee407de1..92e792ee 100644
--- a/src/localization/transpose_chromatic/Base.lua
+++ b/src/localization/transpose_chromatic/en.lua
@@ -1,5 +1,5 @@
 --
--- Localization Base.lua for transpose_chromatic.lua
+-- Localization en.lua for transpose_chromatic.lua
 --
 local loc = {
     error_msg_transposition = "Finale is unable to represent some of the transposed pitches. These pitches were left unchanged.",
diff --git a/src/localization/transpose_enharmonic_up/Base.lua b/src/localization/transpose_enharmonic_down/en.lua
similarity index 78%
rename from src/localization/transpose_enharmonic_up/Base.lua
rename to src/localization/transpose_enharmonic_down/en.lua
index fb832f01..094b7d7c 100644
--- a/src/localization/transpose_enharmonic_up/Base.lua
+++ b/src/localization/transpose_enharmonic_down/en.lua
@@ -1,5 +1,5 @@
 --
--- Localization Base.lua for transpose_enharmonic_up.lua
+-- Localization en.lua for transpose_enharmonic_down.lua
 --
 local loc = {
     error_msg_transposition = "Finale is unable to represent some of the transposed pitches. These pitches were left unchanged.",
diff --git a/src/localization/transpose_enharmonic_down/Base.lua b/src/localization/transpose_enharmonic_up/en.lua
similarity index 78%
rename from src/localization/transpose_enharmonic_down/Base.lua
rename to src/localization/transpose_enharmonic_up/en.lua
index 5d35feec..a0e49afb 100644
--- a/src/localization/transpose_enharmonic_down/Base.lua
+++ b/src/localization/transpose_enharmonic_up/en.lua
@@ -1,5 +1,5 @@
 --
--- Localization Base.lua for transpose_enharmonic_down.lua
+-- Localization en.lua for transpose_enharmonic_up.lua
 --
 local loc = {
     error_msg_transposition = "Finale is unable to represent some of the transposed pitches. These pitches were left unchanged.",
diff --git a/src/transpose_by_step.lua b/src/transpose_by_step.lua
index 56905aec..fc2f995d 100644
--- a/src/transpose_by_step.lua
+++ b/src/transpose_by_step.lua
@@ -1,6 +1,6 @@
 function plugindef(locale)
     local loc = {}
-    loc.Base = {
+    loc.en = {
         menu = "Transpose By Steps",
         desc = "Transpose by the number of steps given, simplifying the note spelling as needed."
     }
@@ -12,7 +12,7 @@ function plugindef(locale)
         menu = "Transponieren nach Schritten",
         desc = "Transponieren nach der angegebenen Anzahl von Schritten und vereinfachen die Notation nach Bedarf.",
     }
-    local t = locale and loc[locale:sub(1,2)] or loc.Base
+    local t = locale and loc[locale:sub(1,2)] or loc.en
     finaleplugin.RequireSelection = false
     finaleplugin.HandlesUndo = true -- not recognized by JW Lua or RGP Lua v0.55
     finaleplugin.Author = "Robert Patterson"
diff --git a/src/transpose_chromatic.lua b/src/transpose_chromatic.lua
index 9d41277e..f94eb7d3 100644
--- a/src/transpose_chromatic.lua
+++ b/src/transpose_chromatic.lua
@@ -1,6 +1,6 @@
 function plugindef(locale)
     local loc = {}
-    loc.Base = {
+    loc.en = {
         menu = "Transpose Chromatic",
         desc = "Chromatic transposition of selected region (supports microtone systems)."
     }
@@ -12,7 +12,7 @@ function plugindef(locale)
         menu = "Transponieren chromatisch",
         desc = "Chromatische Transposition des ausgewählten Abschnittes (unterstützt Mikrotonsysteme)."
     }
-    local t = locale and loc[locale:sub(1,2)] or loc.Base
+    local t = locale and loc[locale:sub(1,2)] or loc.en
     finaleplugin.RequireSelection = false
     finaleplugin.HandlesUndo = true -- not recognized by JW Lua or RGP Lua v0.55
     finaleplugin.Author = "Robert Patterson"
diff --git a/src/transpose_enharmonic_down.lua b/src/transpose_enharmonic_down.lua
index 86c6b800..82cb64de 100644
--- a/src/transpose_enharmonic_down.lua
+++ b/src/transpose_enharmonic_down.lua
@@ -1,6 +1,6 @@
 function plugindef(locale)
     local loc = {}
-    loc.Base = {
+    loc.en = {
         menu = "Enharmonic Transpose Down",
         desc = "Transpose down enharmonically all notes in the selected region."
     }
@@ -12,7 +12,7 @@ function plugindef(locale)
         menu = "Enharmonische Transposition nach unten",
         desc = "Transponieren alle Noten im ausgewählten Abschnitt enharmonisch nach unten.",
     }
-    local t = locale and loc[locale:sub(1,2)] or loc.Base
+    local t = locale and loc[locale:sub(1,2)] or loc.en
     finaleplugin.RequireSelection = true
     finaleplugin.Author = "Robert Patterson"
     finaleplugin.Copyright = "CC0 https://creativecommons.org/publicdomain/zero/1.0/"
diff --git a/src/transpose_enharmonic_up.lua b/src/transpose_enharmonic_up.lua
index ca68a99e..ed3f81a8 100644
--- a/src/transpose_enharmonic_up.lua
+++ b/src/transpose_enharmonic_up.lua
@@ -1,6 +1,6 @@
 function plugindef(locale)
     local loc = {}
-    loc.Base = {
+    loc.en = {
         menu = "Enharmonic Transpose Up",
         desc = "Transpose up enharmonically all notes in the selected region."
     }
@@ -12,7 +12,7 @@ function plugindef(locale)
         menu = "Enharmonische Transposition nach oben",
         desc = "Transponieren alle Noten im ausgewählten Abschnitt enharmonisch nach oben.",
     }
-    local t = locale and loc[locale:sub(1,2)] or loc.Base
+    local t = locale and loc[locale:sub(1,2)] or loc.en
     finaleplugin.RequireSelection = true
     finaleplugin.Author = "Robert Patterson"
     finaleplugin.Copyright = "CC0 https://creativecommons.org/publicdomain/zero/1.0/"
diff --git a/utilities/localization_tool.lua b/utilities/localization_tool.lua
index 0574dc35..82c2e184 100644
--- a/utilities/localization_tool.lua
+++ b/utilities/localization_tool.lua
@@ -136,6 +136,22 @@ local function create_localized_base_table(file_path)
     return retval
 end
 
+local function generate_key_candidate(input_string)
+    local process_string = input_string:gsub("[%p%c%s]", " ")
+    local words = {}
+    for word in process_string:gmatch("%S+") do
+        table.insert(words, word)
+    end
+    if #words <= 0 then
+        return process_string:lower()
+    end
+    local first_three = {}
+    for i = 1, math.min(3, #words) do
+        table.insert(first_three, words[i])
+    end
+    return table.concat(first_three, "_"):lower()
+end
+
 local function make_flat_table_string(file_path, lang, t)
     local file_name = finale.FCString()
     finale.FCString(file_path):SplitToPathAndFile(nil, file_name)
@@ -145,7 +161,7 @@ local function make_flat_table_string(file_path, lang, t)
     table.insert(concat, "--\n")
     table.insert(concat, "loc = {\n")
     for k, v in pairsbykeys(t) do
-        table.insert(concat, tab_str .. "[\"" .. tostring(k) .. "\"] = \"" .. tostring(v) .. "\",\n")
+        table.insert(concat, tab_str .. generate_key_candidate(v) .. " = \"" .. tostring(v) .. "\",\n")
     end
     table.insert(concat, "}\n\nreturn loc\n")
     return table.concat(concat)
@@ -179,7 +195,7 @@ as a localization.
 ]]
 local function create_localized_base_table_string(file_path)
     local t = create_localized_base_table(file_path)
-    local locale = "Base"
+    local locale = "en"
     local table_text = make_flat_table_string(file_path, locale, t)
     global_contents[file_path] = table_text
     set_edit_text(table_text)
@@ -448,7 +464,7 @@ local function on_plugindef(_control)
                 if not locale_exists then
                     plugindef_function[1] = "function plugindef(locale)"
                 end
-                local locale = "Base"
+                local locale = "en"
                 table.insert(plugindef_function, 2, tab_str .. "local loc = {}")
                 table.insert(plugindef_function, 3, tab_str .. "loc." .. locale .. " = " .. base_strings)
                 table.insert(plugindef_function, 4,

From 5a3054dd1ec579161aa3bb4f863cb575a848e646 Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Thu, 8 Feb 2024 14:15:49 -0600
Subject: [PATCH 56/61] fix lint error

---
 utilities/localization_tool.lua | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/utilities/localization_tool.lua b/utilities/localization_tool.lua
index 82c2e184..c85b1f1a 100644
--- a/utilities/localization_tool.lua
+++ b/utilities/localization_tool.lua
@@ -161,7 +161,7 @@ local function make_flat_table_string(file_path, lang, t)
     table.insert(concat, "--\n")
     table.insert(concat, "loc = {\n")
     for k, v in pairsbykeys(t) do
-        table.insert(concat, tab_str .. generate_key_candidate(v) .. " = \"" .. tostring(v) .. "\",\n")
+        table.insert(concat, tab_str .. generate_key_candidate(k) .. " = \"" .. tostring(v) .. "\",\n")
     end
     table.insert(concat, "}\n\nreturn loc\n")
     return table.concat(concat)

From 942fa217a852d899a12ad266b774080dca80b64b Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Thu, 8 Feb 2024 22:08:35 -0600
Subject: [PATCH 57/61] add table support for multi-string arguments methods.

---
 src/library/mixin_helper.lua    | 17 +++++++++++++++--
 utilities/localization_tool.lua |  2 +-
 2 files changed, 16 insertions(+), 3 deletions(-)

diff --git a/src/library/mixin_helper.lua b/src/library/mixin_helper.lua
index 3eaf1b76..5da0eb11 100644
--- a/src/library/mixin_helper.lua
+++ b/src/library/mixin_helper.lua
@@ -636,17 +636,30 @@ Process multiple string arguments.
 
 @ self (class instance)
 @ method_func (function) A method on the class that accepts a single Lua `string` or `FCString` instance
-@ ... (FCStrings | FCString | string | number)
+@ ... (FCStrings | FCString | string | number | table)
 ]]
 function mixin_helper.process_string_arguments(self, method_func, ...)
+    local function to_key_string(value)
+        if type(value) == "string" then
+            value = "\"" .. value .. "\""
+        end
+    
+        return "[" .. tostring(value) .. "]"
+    end
     for i = 1, select("#", ...) do
         local v = select(i, ...)
-        mixin_helper.assert_argument_type(i + 1, v, "string", "number", "FCString", "FCStrings")
+        mixin_helper.assert_argument_type(i + 1, v, "string", "number", "FCString", "FCStrings", "table")
 
         if type(v) == "userdata" and v:ClassName() == "FCStrings" then
             for str in each(v) do
                 method_func(self, str)
             end
+        elseif type(v) == "table" then
+            for k2, v2 in pairsbykeys(v) do
+                require('mobdebug').start()
+                mixin_helper.assert_argument_type(tostring(i + 1) .. to_key_string(k2), v2, "string", "number", "FCString")
+                method_func(self, v2)
+            end
         else
             method_func(self, v)
         end
diff --git a/utilities/localization_tool.lua b/utilities/localization_tool.lua
index c85b1f1a..73962606 100644
--- a/utilities/localization_tool.lua
+++ b/utilities/localization_tool.lua
@@ -506,7 +506,7 @@ local function create_dialog()
         :AddHandleCommand(on_popup)
     dlg:CreateComboBox(0, curr_y, "lang_list")
         :DoAutoResizeWidth(0)
-        :AddStrings(table.unpack(utils.create_keys_table(finale_supported_languages)))
+        :AddStrings(utils.create_keys_table(finale_supported_languages))
         :SetText("Spanish")
     curr_y = curr_y + button_height
     --editor

From 67be045b3ed96015754cccc27d7004527075970f Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Thu, 8 Feb 2024 22:27:30 -0600
Subject: [PATCH 58/61] changed `process_string_arguments` to
 `create_multi_string_proxy` so that errors are reported at the correct level,
 rather than in `tryfunczzz`.

---
 src/library/mixin_helper.lua  | 43 ++++++++++++++++++-----------------
 src/mixin/FCMCtrlComboBox.lua | 14 ++++--------
 src/mixin/FCMCtrlListBox.lua  | 10 +++-----
 src/mixin/FCMCtrlPopup.lua    | 12 ++++------
 4 files changed, 34 insertions(+), 45 deletions(-)

diff --git a/src/library/mixin_helper.lua b/src/library/mixin_helper.lua
index 5da0eb11..580bd034 100644
--- a/src/library/mixin_helper.lua
+++ b/src/library/mixin_helper.lua
@@ -630,38 +630,39 @@ function mixin_helper.create_localized_proxy(method_name, class_name, only_local
 end
 
 --[[
-% process_string_arguments
+% create_multi_string_proxy
 
-Process multiple string arguments.
+Creates a proxy method that takes multiple string arguments.
 
-@ self (class instance)
-@ method_func (function) A method on the class that accepts a single Lua `string` or `FCString` instance
-@ ... (FCStrings | FCString | string | number | table)
+@ method_name (string) An instance method on the class that accepts a single Lua `string`, `FCString`, or `number`
+: (function)
 ]]
-function mixin_helper.process_string_arguments(self, method_func, ...)
+function mixin_helper.create_multi_string_proxy(method_name)
     local function to_key_string(value)
         if type(value) == "string" then
             value = "\"" .. value .. "\""
         end
-    
+
         return "[" .. tostring(value) .. "]"
     end
-    for i = 1, select("#", ...) do
-        local v = select(i, ...)
-        mixin_helper.assert_argument_type(i + 1, v, "string", "number", "FCString", "FCStrings", "table")
+    return function(self, ...)
+        for i = 1, select("#", ...) do
+            local v = select(i, ...)
+            mixin_helper.assert_argument_type(i + 1, v, "string", "number", "FCString", "FCStrings", "table")
 
-        if type(v) == "userdata" and v:ClassName() == "FCStrings" then
-            for str in each(v) do
-                method_func(self, str)
-            end
-        elseif type(v) == "table" then
-            for k2, v2 in pairsbykeys(v) do
-                require('mobdebug').start()
-                mixin_helper.assert_argument_type(tostring(i + 1) .. to_key_string(k2), v2, "string", "number", "FCString")
-                method_func(self, v2)
+            if type(v) == "userdata" and v:ClassName() == "FCStrings" then
+                for str in each(v) do
+                    self[method_name](self, str)
+                end
+            elseif type(v) == "table" then
+                for k2, v2 in pairsbykeys(v) do
+                    require('mobdebug').start()
+                    mixin_helper.assert_argument_type(tostring(i + 1) .. to_key_string(k2), v2, "string", "number", "FCString")
+                    self[method_name](self, v2)
+                end
+            else
+                self[method_name](self, v)
             end
-        else
-            method_func(self, v)
         end
     end
 end
diff --git a/src/mixin/FCMCtrlComboBox.lua b/src/mixin/FCMCtrlComboBox.lua
index c0294cf3..022cc583 100644
--- a/src/mixin/FCMCtrlComboBox.lua
+++ b/src/mixin/FCMCtrlComboBox.lua
@@ -19,7 +19,7 @@ For that reason, this module (at least for now) does not manage those properties
 - Added `AddStrings` that accepts multiple arguments of `table`, `FCString`, Lua `string`, or `number`.
 - Added localized versions `AddStringLocalized` and `AddStringsLocalized`.
 ]] --
-local mixin = require("library.mixin")
+local mixin = require("library.mixin") -- luacheck: ignore
 local mixin_helper = require("library.mixin_helper")
 
 local class = {Methods = {}}
@@ -67,11 +67,9 @@ methods.AddStringLocalized = mixin_helper.create_localized_proxy("AddString")
 Adds multiple strings to the combobox.
 
 @ self (FCMCtrlComboBox)
-@ ... (FCStrings | FCString | string | number)
+@ ... (FCStrings | FCString | string | number | table)
 ]]
-function methods:AddStrings(...)
-    mixin_helper.process_string_arguments(self, mixin.FCMCtrlComboBox.AddString, ...)
-end
+methods.AddStrings = mixin_helper.create_multi_string_proxy("AddString")
 
 --[[
 % AddStringsLocalized
@@ -81,10 +79,8 @@ end
 Adds multiple localized strings to the combobox.
 
 @ self (FCMCtrlComboBox)
-@ ... (FCStrings | FCString | string | number) keys of strings to be added. If no localization is found, the key is added.
+@ ... (FCStrings | FCString | string | number | table) keys of strings to be added. If no localization is found, the key is added.
 ]]
-function methods:AddStringsLocalized(...)
-    mixin_helper.process_string_arguments(self, mixin.FCMCtrlComboBox.AddStringLocalized, ...)
-end
+methods.AddStringsLocalized = mixin_helper.create_multi_string_proxy("AddStringLocalized")
 
 return class
diff --git a/src/mixin/FCMCtrlListBox.lua b/src/mixin/FCMCtrlListBox.lua
index 3c7aa63e..3fea7d98 100644
--- a/src/mixin/FCMCtrlListBox.lua
+++ b/src/mixin/FCMCtrlListBox.lua
@@ -269,11 +269,9 @@ methods.AddStringLocalized = mixin_helper.create_localized_proxy("AddString")
 Adds multiple strings to the list box.
 
 @ self (FCMCtrlListBox)
-@ ... (FCStrings | FCString | string | number)
+@ ... (FCStrings | FCString | string | number | table)
 ]]
-function methods:AddStrings(...)
-    mixin_helper.process_string_arguments(self, mixin.FCMCtrlListBox.AddString, ...)
-end
+methods.AddStrings = mixin_helper.create_multi_string_proxy("AddString")
 
 --[[
 % AddStringsLocalized
@@ -285,9 +283,7 @@ Adds multiple localized strings to the combobox.
 @ self (FCMCtrlListBox)
 @ ... (FCStrings | FCString | string | number) keys of strings to be added. If no localization is found, the key is added.
 ]]
-function methods:AddStringsLocalized(...)
-    mixin_helper.process_string_arguments(self, mixin.FCMCtrlComboBox.AddStringLocalized, ...)
-end
+methods.AddStringsLocalized = mixin_helper.create_multi_string_proxy("AddStringLocalized")
 
 --[[
 % GetStrings
diff --git a/src/mixin/FCMCtrlPopup.lua b/src/mixin/FCMCtrlPopup.lua
index 1b929e01..ad03fb52 100644
--- a/src/mixin/FCMCtrlPopup.lua
+++ b/src/mixin/FCMCtrlPopup.lua
@@ -259,11 +259,9 @@ methods.AddStringLocalized = mixin_helper.create_localized_proxy("AddString")
 Adds multiple strings to the popup.
 
 @ self (FCMCtrlPopup)
-@ ... (FCStrings | FCString | string | number)
+@ ... (FCStrings | FCString | string | number | table)
 ]]
-function methods:AddStrings(...)
-    mixin_helper.process_string_arguments(self, mixin.FCMCtrlPopup.AddString, ...)
-end
+methods.AddStrings = mixin_helper.create_multi_string_proxy("AddString")
 
 --[[
 % AddStringsLocalized
@@ -273,11 +271,9 @@ end
 Adds multiple localized strings to the popup.
 
 @ self (FCMCtrlPopup)
-@ ... (FCStrings | FCString | string | number) keys of strings to be added. If no localization is found, the key is added.
+@ ... (FCStrings | FCString | string | number | table) keys of strings to be added. If no localization is found, the key is added.
 ]]
-function methods:AddStringsLocalized(...)
-    mixin_helper.process_string_arguments(self, mixin.FCMCtrlPopup.AddStringLocalized, ...)
-end
+methods.AddStringsLocalized = mixin_helper.create_multi_string_proxy("AddStringLocalized")
 
 --[[
 % GetStrings

From c77beac015deeb9fa3c85c4d4ad3c83aadfdba27 Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Thu, 8 Feb 2024 22:28:59 -0600
Subject: [PATCH 59/61] remove mobdebug

---
 src/library/mixin_helper.lua | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/library/mixin_helper.lua b/src/library/mixin_helper.lua
index 580bd034..70777f7f 100644
--- a/src/library/mixin_helper.lua
+++ b/src/library/mixin_helper.lua
@@ -656,7 +656,6 @@ function mixin_helper.create_multi_string_proxy(method_name)
                 end
             elseif type(v) == "table" then
                 for k2, v2 in pairsbykeys(v) do
-                    require('mobdebug').start()
                     mixin_helper.assert_argument_type(tostring(i + 1) .. to_key_string(k2), v2, "string", "number", "FCString")
                     self[method_name](self, v2)
                 end

From 8a87b51824543b08feb95a23176a6dcd6fdaa139 Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Fri, 9 Feb 2024 06:54:05 -0600
Subject: [PATCH 60/61] fix small issues discovered

---
 src/library/mixin_helper.lua |  4 +++-
 src/mixin/FCMStrings.lua     | 38 ++++++++++++++++++------------------
 2 files changed, 22 insertions(+), 20 deletions(-)

diff --git a/src/library/mixin_helper.lua b/src/library/mixin_helper.lua
index 70777f7f..c0fcda65 100644
--- a/src/library/mixin_helper.lua
+++ b/src/library/mixin_helper.lua
@@ -139,7 +139,8 @@ The followimg types can be specified:
 
 *NOTE: This function will only assert if in debug mode (ie `finenv.DebugEnabled == true`). If assertions are always required, use `force_assert_argument_type` instead.*
 
-@ argument_number (number) The REAL argument number for the error message (self counts as argument #1).
+@ argument_number (number | string) The REAL argument number for the error message (self counts as argument #1). If the argument is a string, it should
+start with a number.
 @ value (any) The value to test.
 @ ... (string) Valid types (as many as needed). Can be standard Lua types, Finale class names, or mixin class names.
 ]]
@@ -646,6 +647,7 @@ function mixin_helper.create_multi_string_proxy(method_name)
         return "[" .. tostring(value) .. "]"
     end
     return function(self, ...)
+        mixin_helper.assert_argument_type(1, self, "userdata")
         for i = 1, select("#", ...) do
             local v = select(i, ...)
             mixin_helper.assert_argument_type(i + 1, v, "string", "number", "FCString", "FCStrings", "table")
diff --git a/src/mixin/FCMStrings.lua b/src/mixin/FCMStrings.lua
index cc323b65..fd7f3c9a 100644
--- a/src/mixin/FCMStrings.lua
+++ b/src/mixin/FCMStrings.lua
@@ -9,7 +9,7 @@ $module FCMStrings
 - Added polyfill for `CopyFromStringTable`.
 - Added `CreateStringTable` method.
 ]] --
-local mixin = require("library.mixin")
+local mixin = require("library.mixin") -- luacheck: ignore
 local mixin_helper = require("library.mixin_helper")
 
 local class = {Methods = {}}
@@ -32,7 +32,17 @@ Override Changes:
 function methods:AddCopy(str)
     mixin_helper.assert_argument_type(2, str, "string", "number", "FCString")
 
-    mixin_helper.boolean_to_error(self, "AddCopy", mixin_helper.to_fcstring(str, temp_str))
+    str = mixin_helper.to_fcstring(str, temp_str)
+
+    -- versions of Finale Lua before 0.71 always return false. This was a long-standing
+    -- bug in the PDK Framework. For these versions, ignore the return value and make
+    -- the function fluid.
+
+    if finenv.MajorVersion > 0 or finenv.MinorVersion >= 71 then
+        mixin_helper.boolean_to_error(self, "AddCopy", str)
+    else
+        self:AddCopy__(str)
+    end
 end
 
 --[[
@@ -41,21 +51,9 @@ end
 Same as `AddCopy`, but accepts multiple arguments so that multiple values can be added at a time.
 
 @ self (FCMStrings)
-@ ... (FCStrings | FCString | string | number) `number`s will be cast to `string`
+@ ... (FCStrings | FCString | string | number | table) `number`s will be cast to `string`
 ]]
-function methods:AddCopies(...)
-    for i = 1, select("#", ...) do
-        local v = select(i, ...)
-        mixin_helper.assert_argument_type(i + 1, v, "FCStrings", "FCString", "string", "number")
-        if mixin_helper.is_instance_of(v, "FCStrings") then
-            for str in each(v) do
-                self:AddCopy__(str)
-            end
-        else
-            mixin.FCStrings.AddCopy(self, v)
-        end
-    end
-end
+methods.AddCopies = mixin_helper.create_multi_string_proxy("AddCopy")
 
 --[[
 % Find
@@ -72,7 +70,7 @@ Override Changes:
 function methods:Find(str)
     mixin_helper.assert_argument_type(2, str, "string", "number", "FCString")
 
-    return self:Find_(mixin_helper.to_fcstring(str, temp_str))
+    return self:Find__(mixin_helper.to_fcstring(str, temp_str))
 end
 
 --[[
@@ -160,7 +158,7 @@ end
 --[[
 % InsertStringAt
 
-**[>= v0.59] [Fluid] [Override]**
+**[>= v0.68] [Fluid] [Override]**
 
 Override Changes:
 - Accepts Lua `string` and `number` in addition to `FCString`.
@@ -169,7 +167,9 @@ Override Changes:
 @ str (FCString | string | number)
 @ index (number)
 ]]
-if finenv.MajorVersion > 0 or finenv.MinorVersion >= 59 then
+if finenv.MajorVersion > 0 or finenv.MinorVersion >= 68 then
+    -- NOTE: the version of InsertStringAt before 0.68 was not safe, and
+    --          this function would have crashed Finale.
     function methods:InsertStringAt(str, index)
         mixin_helper.assert_argument_type(2, str, "string", "number", "FCString")
         mixin_helper.assert_argument_type(3, index, "number")

From 44e53dce81634c4fd99889e522509b967e524b04 Mon Sep 17 00:00:00 2001
From: Robert Patterson <robert@robertgpatterson.com>
Date: Fri, 9 Feb 2024 09:58:24 -0600
Subject: [PATCH 61/61] Changes for `*AutoLocalized` mixin functions.

---
 samples/auto_layout.lua                     | 18 +++---
 src/library/localization.lua                | 51 +++++++++++++---
 src/library/mixin_helper.lua                |  2 +-
 src/library/utils.lua                       | 15 ++++-
 src/localization/transpose_by_step/de.lua   |  2 -
 src/localization/transpose_by_step/en.lua   |  2 -
 src/localization/transpose_by_step/es.lua   |  2 -
 src/localization/transpose_chromatic/de.lua |  2 -
 src/localization/transpose_chromatic/en.lua |  2 -
 src/localization/transpose_chromatic/es.lua |  2 -
 src/mixin/FCMCustomWindow.lua               | 64 ++++++++++++++++++++-
 src/transpose_by_step.lua                   |  8 +--
 src/transpose_chromatic.lua                 |  8 +--
 13 files changed, 133 insertions(+), 45 deletions(-)

diff --git a/samples/auto_layout.lua b/samples/auto_layout.lua
index c6fbc472..13f44630 100644
--- a/samples/auto_layout.lua
+++ b/samples/auto_layout.lua
@@ -5,7 +5,6 @@ function plugindef()
 end
 
 local utils = require('library.utils')
-local mixin = require('library.mixin')
 local localization = require('library.localization')
 
 --
@@ -22,7 +21,7 @@ localization.en = -- this is en_GB due to spelling of "Localisation"
 {
     action_button = "Action Button",
     choices = "Choices",
-    close = "Close",
+    --close = "Close", -- provide by mixin
     first_option = "First Option",
     fourth_option = "Fourth Option",
     left_checkbox1 = "Left Checkbox Option 1",
@@ -49,7 +48,7 @@ localization.en_US =
 localization.es = {
     action_button = "Botón de Acción",
     choices = "Opciones",
-    close = "Cerrar",
+    -- close = "Cerrar", -- provided by mixin
     first_option = "Primera Opción",
     fourth_option = "Cuarta Opción",
     left_checkbox1 = "Opción de Casilla de Verificación Izquierda 1",
@@ -93,7 +92,7 @@ localization.ja = {
 localization.de = {
     action_button = "Aktionsknopf",
     choices = "Auswahlmöglichkeiten",
-    close = "Schließen",
+    --close = "Schließen", -- provided by mixin
     first_option = "Erste Option",
     fourth_option = "Vierte Option",
     left_checkbox1 = "Linke Checkbox Option 1",
@@ -185,7 +184,12 @@ localization.fa = {
     longer_option_text = "این متن گزینه طولانی تر است %d",
 }
 
-localization.set_locale("es")
+-- mixin should be required after any embedded localizations, to allow the
+-- *AutoLocalized functions to work. This only matters when localizations are
+-- embedded in the main script.
+local mixin = require('library.mixin')
+
+localization.set_locale("fa")
 
 function create_dialog()
     local dlg = mixin.FCXCustomLuaWindow()
@@ -329,9 +333,7 @@ function create_dialog()
     end
     line_no = line_no + 2
 
-    dlg:CreateCloseButton(0, line_no * y_increment + 5)
-        :SetTextLocalized("close")
-        :DoAutoResizeWidth()
+    dlg:CreateCloseButtonAutoLocalized(0, line_no * y_increment + 5)
         :HorizontallyAlignRightWithFurthest()
 
     return dlg
diff --git a/src/library/localization.lua b/src/library/localization.lua
index dc9333a1..1006d4d8 100644
--- a/src/library/localization.lua
+++ b/src/library/localization.lua
@@ -112,6 +112,7 @@ is running with.
 local localization = {}
 
 local library = require("library.general_library")
+local utils = require("library.utils")
 
 local locale = (function()
         if finenv.UI().GetUserLocaleName then
@@ -126,6 +127,8 @@ local fallback_locale = "en"
 
 local script_name = library.calc_script_name()
 
+local tried_locales = {} -- track which locales we've tried to load
+
 --[[
 % set_locale
 
@@ -176,22 +179,54 @@ function localization.get_fallback_locale()
     return fallback_locale
 end
 
--- This function finds a localization string table if it exists or requires it if it doesn't.
-local function get_localized_table(try_locale)
-    if type(localization[try_locale]) == "table" then
-        return localization[try_locale]
-    end
+local function get_original_locale_table(try_locale)
     local require_library = "localization" .. "." .. script_name .. "." .. try_locale
     local success, result = pcall(function() return require(require_library) end)
     if success and type(result) == "table" then
-        localization[try_locale] = result
-    else
+        return result
+    end
+    return nil
+end
+
+-- This function finds a localization string table if it exists or requires it if it doesn't.
+-- AutoLocalize functions can add key/value pairs separately, so preserve them if they are there.
+local function get_localized_table(try_locale)
+    local table_exists = type(localization[try_locale]) == "table"
+    if not table_exists or not tried_locales[try_locale] then
+        assert(table_exists or type(localization[try_locale]) == "nil",
+                    "incorrect type for localization[" .. try_locale .. "]; got " .. type(localization[try_locale]))
+        local original_table = get_original_locale_table(try_locale)
+        if type(original_table) == "table" then
+            -- this overwrites previously added values if they exist in the newly required localization table,
+            -- but it preserves the previously added values if they don't exist in the newly required table.
+            localization[try_locale] = utils.copy_table(original_table, localization[try_locale])
+        end
         -- doing this allows us to only try to require it once
-        localization[try_locale] = {}
+        tried_locales[try_locale] = true
     end
     return localization[try_locale]
 end
 
+--[[
+% add_to_locale
+
+Adds values to to the locale table, but only if the locale table already exists. If a utility function needs
+to expand a locale table, it should use this function. This function does not replace keys that already exist.
+
+@ (try_locale) the locale to add to
+@ (table) the key/value pairs to add
+: (boolean) true if addded
+]]
+function localization.add_to_locale(try_locale, t)
+    if type(localization[try_locale]) ~= "table" then
+        if not get_original_locale_table(try_locale) then
+            return false
+        end
+    end
+    localization[try_locale] = utils.copy_table(t, localization[try_locale], false)
+    return true
+end
+
 local function try_locale_or_language(try_locale)
     local t = get_localized_table(try_locale)
     if t then
diff --git a/src/library/mixin_helper.lua b/src/library/mixin_helper.lua
index c0fcda65..a0cc4522 100644
--- a/src/library/mixin_helper.lua
+++ b/src/library/mixin_helper.lua
@@ -140,7 +140,7 @@ The followimg types can be specified:
 *NOTE: This function will only assert if in debug mode (ie `finenv.DebugEnabled == true`). If assertions are always required, use `force_assert_argument_type` instead.*
 
 @ argument_number (number | string) The REAL argument number for the error message (self counts as argument #1). If the argument is a string, it should
-start with a number.
+start with a number that is the real argument number.
 @ value (any) The value to test.
 @ ... (string) Valid types (as many as needed). Can be standard Lua types, Finale class names, or mixin class names.
 ]]
diff --git a/src/library/utils.lua b/src/library/utils.lua
index cf8b3231..11977258 100644
--- a/src/library/utils.lua
+++ b/src/library/utils.lua
@@ -11,16 +11,25 @@ local utils = {}
 If a table is passed, returns a copy, otherwise returns the passed value.
 
 @ t (mixed)
+@ [to_table] (table) the existing top-level table to copy to if present. (Sub-tables are always copied to new tables.)
+@ [overwrite] (boolean) if true, overwrites existing values; if false, does not copy over existing values. Default is true.
 : (mixed)
 ]]
 ---@generic T
 ---@param t T
 ---@return T
-function utils.copy_table(t)
+function utils.copy_table(t, to_table, overwrite)
+    overwrite = (overwrite == nil) and true or false
     if type(t) == "table" then
-        local new = {}
+        local new = type(to_table) == "table" and to_table or {}
         for k, v in pairs(t) do
-            new[utils.copy_table(k)] = utils.copy_table(v)
+            local new_key = utils.copy_table(k)
+            local new_value = utils.copy_table(v)
+            if overwrite then
+                new[new_key] = new_value
+            else
+                new[new_key] = new[new_key] == nil and new_value or new[new_key]
+            end
         end
         setmetatable(new, utils.copy_table(getmetatable(t)))
         return new
diff --git a/src/localization/transpose_by_step/de.lua b/src/localization/transpose_by_step/de.lua
index d2fb066c..43a9aaf3 100644
--- a/src/localization/transpose_by_step/de.lua
+++ b/src/localization/transpose_by_step/de.lua
@@ -5,8 +5,6 @@ local loc = {
     error_msg_transposition = "Finale kann einige der transponierten Tönhöhen nicht darstellen. Diese Tönhöhen wurden unverändert gelassen.",
     number_of_steps = "Anzahl der Schritte",
     transposition_error = "Transpositionsfehler",
-    ok = "OK",
-    cancel = "Abbrechen",
 }
 
 return loc
diff --git a/src/localization/transpose_by_step/en.lua b/src/localization/transpose_by_step/en.lua
index 3be901e0..a5945312 100644
--- a/src/localization/transpose_by_step/en.lua
+++ b/src/localization/transpose_by_step/en.lua
@@ -5,8 +5,6 @@ local loc = {
     error_msg_transposition = "Finale is unable to represent some of the transposed pitches. These pitches were left unchanged.",
     number_of_steps = "Number Of Steps",
     transposition_error = "Transposition Error",
-    ok = "OK",
-    cancel = "Cancel"
 }
 
 return loc
diff --git a/src/localization/transpose_by_step/es.lua b/src/localization/transpose_by_step/es.lua
index 3ab4eb97..18fb1df9 100644
--- a/src/localization/transpose_by_step/es.lua
+++ b/src/localization/transpose_by_step/es.lua
@@ -5,8 +5,6 @@ local loc = {
     error_msg_transposition = "Finale no puede representar algunas de las notas traspuestas. Estas notas no se han cambiado.",
     number_of_steps = "Número De Pasos",
     transposition_error = "Error de trasposición",
-    ok = "Aceptar",
-    canel = "Cancelar"
 }
 
 return loc
diff --git a/src/localization/transpose_chromatic/de.lua b/src/localization/transpose_chromatic/de.lua
index cd236e7e..53c1856f 100644
--- a/src/localization/transpose_chromatic/de.lua
+++ b/src/localization/transpose_chromatic/de.lua
@@ -37,8 +37,6 @@ local loc = {
     simplify_spelling = "Notation vereinfachen",
     transposition_error = "Transpositionsfehler",
     up = "Hoch",
-    ok = "OK",
-    cancel = "Abbrechen",
 }
 
 return loc
diff --git a/src/localization/transpose_chromatic/en.lua b/src/localization/transpose_chromatic/en.lua
index 92e792ee..742af4eb 100644
--- a/src/localization/transpose_chromatic/en.lua
+++ b/src/localization/transpose_chromatic/en.lua
@@ -37,8 +37,6 @@ local loc = {
     simplify_spelling = "Simplify Spelling",
     transposition_error = "Transposition Error",
     up = "Up",
-    ok = "OK",
-    cancel = "Cancel",
 }
 
 return loc
diff --git a/src/localization/transpose_chromatic/es.lua b/src/localization/transpose_chromatic/es.lua
index 6c3c1bb7..bdfe46d7 100644
--- a/src/localization/transpose_chromatic/es.lua
+++ b/src/localization/transpose_chromatic/es.lua
@@ -37,8 +37,6 @@ local loc = {
     simplify_spelling = "Simplificar enarmonización",
     transposition_error = "Error de trasposición",
     up = "Arriba",
-    ok = "Aceptar",
-    cancel = "Cancelar",
 }
 
 return loc
diff --git a/src/mixin/FCMCustomWindow.lua b/src/mixin/FCMCustomWindow.lua
index 17bc646e..09afb542 100644
--- a/src/mixin/FCMCustomWindow.lua
+++ b/src/mixin/FCMCustomWindow.lua
@@ -10,6 +10,7 @@ $module FCMCustomWindow
 ]] --
 local mixin = require("library.mixin")
 local mixin_helper = require("library.mixin_helper")
+local loc = require("library.localization")
 
 local class = {Methods = {}}
 local methods = class.Methods
@@ -344,7 +345,7 @@ for num_args, ctrl_types in pairs({
     for _, control_type in pairs(ctrl_types) do
         local type_exists = false
         if finenv.IsRGPLua then
-            type_exists = finale.FCCustomWindow.__class["Create" .. control_type]      
+            type_exists = finale.FCCustomWindow.__class["Create" .. control_type]
         else
             -- JW Lua crashes if we index the __class table with an invalid key, so instead search it
             for k, _ in pairs(finale.FCCustomWindow.__class) do
@@ -371,6 +372,67 @@ for num_args, ctrl_types in pairs({
     end
 end
 
+--[[
+% CreateCancelButtonAutoLocalized
+
+Localizes the button using the key "cancel". Transalations for English, Spanish, and German are
+provided automatically. Other translations may be added here or in individual localization files
+for the script
+
+@ self (FCMCustomWindow)
+@ [control_name] (FCString | string) Optional name to allow access from `GetControl` method.
+: (FCMCtrlButton)
+]]
+
+--[[
+% CreateOkButtonAutoLocalized
+
+Localizes the button using the key "ok". Transalations for English, Spanish, and German are
+provided automatically. Other translations may be added here or in individual localization files
+for the script
+
+@ self (FCMCustomWindow)
+@ [control_name] (FCString | string) Optional name to allow access from `GetControl` method.
+: (FCMCtrlButton)
+]]
+
+--[[
+% CreateCloseButtonAutoLocalized
+
+**[>= v0.56]**
+
+Localizes the button using the key "cancel". Transalations for English, Spanish, and German are
+provided automatically. Other translations may be added here or in individual localization files
+for the script
+
+@ self (FCMCustomWindow)
+@ x (number)
+@ y (number)
+@ [control_name] (FCString|string) Optional name to allow access from `GetControl` method.
+: (FCMCtrlButton)
+]]
+loc.add_to_locale("en", { ok = "OK", cancel = "Cancel", close = "Close" })
+loc.add_to_locale("es", { ok = "Aceptar", cancel = "Cancelar", close = "Cerrar" })
+loc.add_to_locale("de", { ok = "OK", cancel = "Abbrechen", close = "Schließen" })
+for num_args, method_info in pairs({
+    [0] = { CancelButton = "cancel", OkButton = "ok" },
+    [2] = { CloseButton = "close" },
+})
+do
+    for method_name, localization_key in pairs(method_info) do
+        methods["Create" .. method_name .. "AutoLocalized"] = function(self, ...)
+            for i = 1, num_args do
+                mixin_helper.assert_argument_type(i + 1, select(i, ...), "number")
+            end
+            mixin_helper.assert_argument_type(num_args + 2, select(num_args + 1, ...), "string", "nil", "FCString")
+
+            return self["Create" .. method_name](self, ...)
+                :SetTextLocalized(localization_key)
+                :_FallbackCall("DoAutoResizeWidth", nil)
+        end
+    end
+end
+
 --[[
 % FindControl
 
diff --git a/src/transpose_by_step.lua b/src/transpose_by_step.lua
index fc2f995d..e4a545aa 100644
--- a/src/transpose_by_step.lua
+++ b/src/transpose_by_step.lua
@@ -102,12 +102,8 @@ function create_dialog_box()
         :SetText("")
         :_FallbackCall("AssureNoHorizontalOverlap", nil, dialog:GetControl("steps_label"), 5)
     -- ok/cancel
-    dialog:CreateOkButton()
-        :SetTextLocalized("ok")
-        :_FallbackCall("DoAutoResizeWidth", nil)
-    dialog:CreateCancelButton()
-        :SetTextLocalized("cancel")
-        :_FallbackCall("DoAutoResizeWidth", nil)
+    dialog:CreateOkButtonAutoLocalized()
+    dialog:CreateCancelButtonAutoLocalized()
     -- registrations
     dialog:RegisterHandleOkButtonPressed(function(self)
         do_transpose_by_step(self:GetControl("num_steps"):GetInteger())
diff --git a/src/transpose_chromatic.lua b/src/transpose_chromatic.lua
index f94eb7d3..0601c7e1 100644
--- a/src/transpose_chromatic.lua
+++ b/src/transpose_chromatic.lua
@@ -185,12 +185,8 @@ function create_dialog_box()
         :_FallbackCall("DoAutoResizeWidth", nil)
     current_y = current_y + y_increment -- luacheck: ignore
     -- OK/Cxl
-    dialog:CreateOkButton()
-        :SetTextLocalized("ok")
-        :_FallbackCall("DoAutoResizeWidth", nil)
-    dialog:CreateCancelButton()
-        :SetTextLocalized("cancel")
-        :_FallbackCall("DoAutoResizeWidth", nil)
+    dialog:CreateOkButtonAutoLocalized()
+    dialog:CreateCancelButtonAutoLocalized()
     -- registrations
     dialog:RegisterHandleOkButtonPressed(function(self)
             local direction = 1 -- up