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() + 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 diff --git a/.luacheckrc b/.luacheckrc index 9d15754f..faeecadd 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", } diff --git a/.vscode/recommended_settings.json b/.vscode/recommended_settings.json index eefd1bd2..a6d432c6 100644 --- a/.vscode/recommended_settings.json +++ b/.vscode/recommended_settings.json @@ -14,16 +14,17 @@ "loadall", "loadallforregion", "pairsbykeys", + "prettyformatjson", "bit32", "utf8", "socket", "tinyxml2", "xmlelements", - "xmlattributes", - "prettyformatjson" + "xmlattributes" ], "files.exclude": { "mobdebug.lua": true, + "dist/**": true }, "editor.formatOnSave": true, "Lua.diagnostics.enable": false, diff --git a/docs/rgp-lua.md b/docs/rgp-lua.md index 129c10ae..31d2781b 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) + finaleplugin.RequireSelection = true + finaleplugin.CategoryTags = "Rest, Region" + local localization = {} + localization.en = { + menu = "Hide Rests", + desc = "Hides all rests in the selected region." + } + localization.es = { + menu = "Ocultar Silencios", + desc = "Oculta todos los silencios en la región seleccionada." + } + localization.jp = { + menu = "休符を隠す", + desc = "選択した領域内のすべての休符を隠します。", + } + -- add more localizations as desired + local t = locale and localization[locale:sub(1,2)] or localization.en + return t.menu, t.menu, t.desc +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/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 diff --git a/samples/auto_layout.lua b/samples/auto_layout.lua new file mode 100644 index 00000000..13f44630 --- /dev/null +++ b/samples/auto_layout.lua @@ -0,0 +1,344 @@ +function plugindef() + finaleplugin.RequireDocument = false + finaleplugin.MinJWLuaVersion = 0.71 + return "0--auto resize width test" +end + +local utils = require('library.utils') +local localization = require('library.localization') + +-- +-- 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" +{ + action_button = "Action Button", + choices = "Choices", + --close = "Close", -- provide by mixin + 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 = "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", -- provided by mixin + 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.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", -- provided by mixin + 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_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_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_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_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", +} + +-- 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() + dlg:SetTitleLocalized("test_autolayout") + + local line_no = 0 + local y_increment = 22 + local label_edit_separ = 3 + local center_padding = 20 + + -- left side + dlg:CreateStatic(0, line_no * y_increment, "option1-label") + :DoAutoResizeWidth(0) + :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) + line_no = line_no + 1 + + dlg:CreateCheckbox(0, line_no * y_increment, "left-checkbox1") + :DoAutoResizeWidth(0) + :SetTextLocalized("left_checkbox1") + line_no = line_no + 1 + + dlg:CreateStatic(0, line_no * y_increment, "option2-label") + :DoAutoResizeWidth(0) + :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) + :HorizontallyAlignLeftWith(dlg:GetControl("option1")) + line_no = line_no + 1 + + dlg:CreateCheckbox(0, line_no * y_increment, "left-checkbox2") + :DoAutoResizeWidth(0) + :SetTextLocalized("left_checkbox2") + line_no = line_no + 1 + + -- 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) + :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(0) + :SetTextLocalized("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(0) + :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") + :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) + :HorizontallyAlignLeftWith(dlg:GetControl("option3")) + line_no = line_no + 1 + + dlg:CreateButton(0, line_no * y_increment) + :DoAutoResizeWidth() + :SetTextLocalized("action_button") + :AssureNoHorizontalOverlap(vertical_line, center_padding) + :HorizontallyAlignRightWith(dlg:GetControl("option4")) +-- :HorizontallyAlignRightWithFurthest() + 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 + local start_line_no = line_no + dlg:CreateStatic(0, line_no * y_increment, "popup_label") + :DoAutoResizeWidth(0) + :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 + format_string = localization.localize("long_menu_text") + else + 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") + 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 + format_string = localization.localize("long_text_choice") + else + 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 + + 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:DoAutoResizeWidth(0) + :AssureNoHorizontalOverlap(ctrl_popup, 10) + :AssureNoHorizontalOverlap(ctrl_cbobox, 10) + local format_string + if counter == 2 then + format_string = localization.localize("longer_option_text") + else + format_string = localization.localize("short") + end + rbtn:SetText(string.format(format_string, counter)) + counter = counter + 1 + end + line_no = line_no + 2 + + dlg:CreateCloseButtonAutoLocalized(0, line_no * y_increment + 5) + :HorizontallyAlignRightWithFurthest() + + return dlg +end + +global_dialog = global_dialog or create_dialog() +global_dialog:RunModeless() +--global_dialog:ExecuteModal(nil) diff --git a/src/baseline_move_reset.lua b/src/baseline_move_reset.lua index f7e28806..a2a1d169 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,10 @@ 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.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 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 +123,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") 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..1006d4d8 --- /dev/null +++ b/src/library/localization.lua @@ -0,0 +1,270 @@ +--[[ +$module Localization + +This library provides localization services to scripts. Note that this library cannot be used inside +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` +library directly and wrap any user-facing string in a call to `localization.localize`. + +**Details** + +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). + +``` +src/ + my_highly_useful_script.lua + localization/ + my_highly_useful_script/ + de.lua + en.lua + es.lua + es_ES.lua + jp.lua + ... + +``` + +Each localization lua should return a table of keys and translations. + +English: + +``` +-- +-- en.lua: +-- +local t = { + hello = "Hello", + goodbye = "Goodbye", + computer = "Computer" +} + + +Japanese: + +``` +-- +-- jp.lua: +-- +local t = { + hello = "今日は", + goodbye = "さようなら", + computer = "コンピュータ" +} + +return t +``` + +Spanish: + +``` +-- +-- es.lua: +-- +local t = { + hello = "Hola", + goodbye = "Adiós", + computer = "Computadora" +} + +return t +``` + +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 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 +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 returns strings that are the closest match to the locale the library +is running with. +]] + +local localization = {} + +local library = require("library.general_library") +local utils = require("library.utils") + +local locale = (function() + if finenv.UI().GetUserLocaleName then + local fcstr = finale.FCString() + finenv.UI():GetUserLocaleName(fcstr) + return fcstr.LuaString:gsub("-", "_") + end + return "en_US" + end)() + +local fallback_locale = "en" + +local script_name = library.calc_script_name() + +local tried_locales = {} -- track which locales we've tried to load + +--[[ +% 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 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. + +@ input_locale (string) the 2-letter lowercase language code or 5-character regional locale code +]] +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 + +--[[ +% 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 + +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 + 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 + 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 + 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 + +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(input_string) == "string", "expected string, got " .. type(input_string)) + + if locale == nil then + return input_string + end + assert(type(locale) == "string", "invalid locale setting " .. tostring(locale)) + + local t = try_locale_or_language(locale) + if t and t[input_string] then + return t[input_string] + end + + t = get_localized_table(fallback_locale) + + return t and t[input_string] or input_string +end + +return localization diff --git a/src/library/mixin_helper.lua b/src/library/mixin_helper.lua index 66b67040..a0cc4522 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 = {} @@ -138,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 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. ]] @@ -557,10 +559,27 @@ 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 +--[[ +% to_string + +Casts a value to a Lua string. If the value is an `FCString`, it returns `LuaString`, otherwise it calls `tostring`. + +@ value (any) +: (string) +]] + +function mixin_helper.to_string(value) + if mixin_helper.is_instance_of(value, "FCString") then + return value.LuaString + end + + return value == nil and "" or tostring(value) +end + --[[ % boolean_to_error @@ -578,4 +597,76 @@ 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 + +--[[ +% create_multi_string_proxy + +Creates a proxy method that takes multiple string arguments. + +@ method_name (string) An instance method on the class that accepts a single Lua `string`, `FCString`, or `number` +: (function) +]] +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 + 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") + + 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 + 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 + end + end +end + + return mixin_helper 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 diff --git a/src/library/utils.lua b/src/library/utils.lua index b6f6d28c..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 @@ -63,6 +72,41 @@ function utils.iterate_keys(t) end end +--[[ +% get_keys + +Returns a sorted array table of all the keys in a table. + +@ t (table) +: (table) array table of the keys +]] +function utils.create_keys_table(t) + local retval = {} + + for k, _ in pairsbykeys(t) do + table.insert(retval, k) + end + 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 @@ -383,4 +427,21 @@ function utils.show_notes_dialog(caption, width, height) end 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 diff --git a/src/localization/transpose_by_step/de.lua b/src/localization/transpose_by_step/de.lua new file mode 100644 index 00000000..43a9aaf3 --- /dev/null +++ b/src/localization/transpose_by_step/de.lua @@ -0,0 +1,10 @@ +-- +-- Localization de.lua for transpose_by_step.lua +-- +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", +} + +return loc diff --git a/src/localization/transpose_by_step/en.lua b/src/localization/transpose_by_step/en.lua new file mode 100644 index 00000000..a5945312 --- /dev/null +++ b/src/localization/transpose_by_step/en.lua @@ -0,0 +1,10 @@ +-- +-- 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.", + number_of_steps = "Number Of Steps", + transposition_error = "Transposition Error", +} + +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..18fb1df9 --- /dev/null +++ b/src/localization/transpose_by_step/es.lua @@ -0,0 +1,10 @@ +-- +-- Localization es.lua for transpose_by_step.lua +-- +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", +} + +return loc diff --git a/src/localization/transpose_chromatic/de.lua b/src/localization/transpose_chromatic/de.lua new file mode 100644 index 00000000..53c1856f --- /dev/null +++ b/src/localization/transpose_chromatic/de.lua @@ -0,0 +1,42 @@ +-- +-- Localization de.lua for transpose_chromatic.lua +-- +local loc = { + 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", +} + +return loc diff --git a/src/localization/transpose_chromatic/en.lua b/src/localization/transpose_chromatic/en.lua new file mode 100644 index 00000000..742af4eb --- /dev/null +++ b/src/localization/transpose_chromatic/en.lua @@ -0,0 +1,42 @@ +-- +-- 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.", + 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", +} + +return loc diff --git a/src/localization/transpose_chromatic/es.lua b/src/localization/transpose_chromatic/es.lua new file mode 100644 index 00000000..bdfe46d7 --- /dev/null +++ b/src/localization/transpose_chromatic/es.lua @@ -0,0 +1,42 @@ +-- +-- Localization es.lua for transpose_chromatic.lua +-- +local loc = { + 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", +} + +return loc diff --git a/src/localization/transpose_enharmonic_down/de.lua b/src/localization/transpose_enharmonic_down/de.lua new file mode 100644 index 00000000..19224f13 --- /dev/null +++ b/src/localization/transpose_enharmonic_down/de.lua @@ -0,0 +1,9 @@ +-- +-- Localization de.lua for transpose_enharmonic_down.lua +-- +local loc = { + 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/en.lua b/src/localization/transpose_enharmonic_down/en.lua new file mode 100644 index 00000000..094b7d7c --- /dev/null +++ b/src/localization/transpose_enharmonic_down/en.lua @@ -0,0 +1,9 @@ +-- +-- 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.", + transposition_error = "Transposition Error" +} + +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..7ceabf2f --- /dev/null +++ b/src/localization/transpose_enharmonic_down/es.lua @@ -0,0 +1,9 @@ +-- +-- Localization es.lua for transpose_enharmonic_down.lua +-- +local loc = { + 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/de.lua b/src/localization/transpose_enharmonic_up/de.lua new file mode 100644 index 00000000..fa583027 --- /dev/null +++ b/src/localization/transpose_enharmonic_up/de.lua @@ -0,0 +1,9 @@ +-- +-- Localization de.lua for transpose_enharmonic_up.lua +-- +local loc = { + 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/en.lua b/src/localization/transpose_enharmonic_up/en.lua new file mode 100644 index 00000000..a0e49afb --- /dev/null +++ b/src/localization/transpose_enharmonic_up/en.lua @@ -0,0 +1,9 @@ +-- +-- 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.", + transposition_error = "Transposition Error" +} + +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..e56beb1c --- /dev/null +++ b/src/localization/transpose_enharmonic_up/es.lua @@ -0,0 +1,9 @@ +-- +-- Localization es.lua for transpose_enharmonic_up.lua +-- +local loc = { + 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/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 7e868471..bbe759d9 100644 --- a/src/mixin/FCMControl.lua +++ b/src/mixin/FCMControl.lua @@ -395,4 +395,16 @@ Removes a handler added with `AddHandleCommand`. ]] methods.AddHandleCommand, methods.RemoveHandleCommand = mixin_helper.create_standard_control_event("HandleCommand") +--[[ +% SetTextLocalized + +**[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. +]] +methods.SetTextLocalized = mixin_helper.create_localized_proxy("SetText", "FCMControl") + return class diff --git a/src/mixin/FCMCtrlComboBox.lua b/src/mixin/FCMCtrlComboBox.lua new file mode 100644 index 00000000..022cc583 --- /dev/null +++ b/src/mixin/FCMCtrlComboBox.lua @@ -0,0 +1,86 @@ +-- 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 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") + +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 (FCMCtrlComboBox) +@ 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 + +--[[ +% AddStringLocalized + +**[Fluid]** + +Localized version of `AddString`. + +@ 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") + +--[[ +% AddStrings + +**[Fluid]** + +Adds multiple strings to the combobox. + +@ self (FCMCtrlComboBox) +@ ... (FCStrings | FCString | string | number | table) +]] +methods.AddStrings = mixin_helper.create_multi_string_proxy("AddString") + +--[[ +% AddStringsLocalized + +**[Fluid]** + +Adds multiple localized strings to the combobox. + +@ self (FCMCtrlComboBox) +@ ... (FCStrings | FCString | string | number | table) keys of strings to be added. If no localization is found, the key is added. +]] +methods.AddStringsLocalized = mixin_helper.create_multi_string_proxy("AddStringLocalized") + +return class diff --git a/src/mixin/FCMCtrlListBox.lua b/src/mixin/FCMCtrlListBox.lua index 087b7d5f..3fea7d98 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 @@ -257,22 +269,21 @@ end Adds multiple strings to the list box. @ self (FCMCtrlListBox) -@ ... (FCStrings | FCString | string | number) +@ ... (FCStrings | FCString | string | number | table) ]] -function methods:AddStrings(...) - for i = 1, select("#", ...) do - local v = select(i, ...) - mixin_helper.assert_argument_type(i + 1, v, "string", "number", "FCString", "FCStrings") +methods.AddStrings = mixin_helper.create_multi_string_proxy("AddString") - 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 -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. +]] +methods.AddStringsLocalized = mixin_helper.create_multi_string_proxy("AddStringLocalized") --[[ % GetStrings diff --git a/src/mixin/FCMCtrlPopup.lua b/src/mixin/FCMCtrlPopup.lua index fb9bac43..ad03fb52 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 @@ -238,6 +239,18 @@ function methods:AddString(str) table.insert(private[self].Items, str.LuaString) end +--[[ +% AddStringLocalized + +**[Fluid]** + +Localized version of `AddString`. + +@ 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") + --[[ % AddStrings @@ -246,22 +259,21 @@ end Adds multiple strings to the popup. @ self (FCMCtrlPopup) -@ ... (FCStrings | FCString | string | number) +@ ... (FCStrings | FCString | string | number | table) ]] -function methods:AddStrings(...) - for i = 1, select("#", ...) do - local v = select(i, ...) - 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 - mixin.FCMCtrlPopup.AddString(self, str) - end - else - mixin.FCMCtrlPopup.AddString(self, v) - end - end -end +methods.AddStrings = mixin_helper.create_multi_string_proxy("AddString") + +--[[ +% AddStringsLocalized + +**[Fluid]** + +Adds multiple localized strings to the popup. + +@ self (FCMCtrlPopup) +@ ... (FCStrings | FCString | string | number | table) keys of strings to be added. If no localization is found, the key is added. +]] +methods.AddStringsLocalized = mixin_helper.create_multi_string_proxy("AddStringLocalized") --[[ % GetStrings 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/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") diff --git a/src/mixin/FCMUI.lua b/src/mixin/FCMUI.lua index 1618991a..d0b4e02d 100644 --- a/src/mixin/FCMUI.lua +++ b/src/mixin/FCMUI.lua @@ -42,4 +42,45 @@ 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 + +**[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. +]] +methods.AlertErrorLocalized = mixin_helper.create_localized_proxy("AlertError") + return class diff --git a/src/mixin/__FCMUserWindow.lua b/src/mixin/__FCMUserWindow.lua index 51e023bd..c79fd8c6 100644 --- a/src/mixin/__FCMUserWindow.lua +++ b/src/mixin/__FCMUserWindow.lua @@ -60,4 +60,35 @@ 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 + +**[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 7e3a350b..e4a545aa 100644 --- a/src/transpose_by_step.lua +++ b/src/transpose_by_step.lua @@ -1,4 +1,18 @@ -function plugindef() +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" @@ -14,23 +28,23 @@ 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, + 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.) - + ``` 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." + return t.menu .. "...", t.menu, t.desc end -- luacheck: ignore 11./global_dialog @@ -43,12 +57,14 @@ end local transposition = require("library.transposition") local mixin = require("library.mixin") +local loc = require("library.localization") +local utils = require("library.utils") 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 = ({plugindef(loc.get_locale())})[2] .. " " .. tostring(finenv.Region().StartMeasure) if finenv.Region().StartMeasure ~= finenv.Region().EndMeasure then undostr = undostr .. " - " .. tostring(finenv.Region().EndMeasure) end @@ -66,26 +82,32 @@ 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") + global_dialog:CreateChildUI():AlertErrorLocalized("error_msg_transposition", "transposition_error") end return success end function create_dialog_box() - local dialog = mixin.FCXCustomLuaWindow():SetTitle("Transpose By Steps") + local dialog = mixin.FCXCustomLuaWindow() + :SetTitle(plugindef(loc.get_locale()):gsub("%.%.%.", "")) 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:CreateEdit(edit_x, current_y, "num_steps"):SetText("") + dialog:CreateStatic(0, current_y + 2, "steps_label") + :SetTextLocalized("number_of_steps") + :SetWidth(x_increment - 5) + :_FallbackCall("DoAutoResizeWidth", nil) + 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"), 5) -- ok/cancel - dialog:CreateOkButton() - dialog:CreateCancelButton() + dialog:CreateOkButtonAutoLocalized() + dialog:CreateCancelButtonAutoLocalized() + -- 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 diff --git a/src/transpose_chromatic.lua b/src/transpose_chromatic.lua index 66b8c702..0601c7e1 100644 --- a/src/transpose_chromatic.lua +++ b/src/transpose_chromatic.lua @@ -1,4 +1,18 @@ -function plugindef() +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,54 +43,11 @@ 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)." + return t.menu .. "...", t.menu, t.desc 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" - } - - 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 - } -end - 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,70 @@ 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:CreateChildUI():AlertErrorLocalized("error_msg_transposition", "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") + :SetTextLocalized("direction") + :SetWidth(x_increment - 5) + :_FallbackCall("DoAutoResizeWidth", nil) + dialog:CreatePopup(x_increment, current_y, "direction_choice") + :AddStringsLocalized("up", "down"):SetWidth(x_increment) + :SetSelectedItem(0) + :_FallbackCall("DoAutoResizeWidth", nil) + :_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") + :SetTextLocalized("interval") + :SetWidth(x_increment - 5) + :_FallbackCall("DoAutoResizeWidth", nil) + dialog:CreatePopup(x_increment, current_y, "interval_choice") + :AddStringsLocalized(table.unpack(interval_names)) + :SetWidth(140) + :SetSelectedItem(0) + :_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"):SetText("Simplify Spelling"):SetWidth(140):SetCheck(0) + dialog:CreateCheckbox(0, current_y + 2, "do_simplify") + :SetTextLocalized("simplify_spelling") + :SetWidth(140) + :SetCheck(0) + :_FallbackCall("DoAutoResizeWidth", nil) 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") + :SetTextLocalized("plus_octaves") + :SetWidth(x_increment - 5) + :_FallbackCall("DoAutoResizeWidth", nil) + 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") + :SetTextLocalized("preserve_existing") + :SetWidth(140) + :SetCheck(0) + :_FallbackCall("DoAutoResizeWidth", nil) + current_y = current_y + y_increment -- luacheck: ignore -- OK/Cxl - dialog:CreateOkButton() - dialog:CreateCancelButton() + dialog:CreateOkButtonAutoLocalized() + dialog:CreateCancelButtonAutoLocalized() + -- registrations dialog:RegisterHandleOkButtonPressed(function(self) local direction = 1 -- up if self:GetControl("direction_choice"):GetSelectedItem() > 0 then diff --git a/src/transpose_enharmonic_down.lua b/src/transpose_enharmonic_down.lua index 7e9ef95c..82cb64de 100644 --- a/src/transpose_enharmonic_down.lua +++ b/src/transpose_enharmonic_down.lua @@ -1,4 +1,18 @@ -function plugindef() +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,11 +40,11 @@ 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." + return t.menu, t.menu, t.desc end local transposition = require("library.transposition") +local loc = require("library.localization") function transpose_enharmonic_down() local success = true @@ -40,7 +54,7 @@ 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("error_msg_transposition"), loc.localize("transposition_error")) end end diff --git a/src/transpose_enharmonic_up.lua b/src/transpose_enharmonic_up.lua index 567d1f3a..ed3f81a8 100644 --- a/src/transpose_enharmonic_up.lua +++ b/src/transpose_enharmonic_up.lua @@ -1,4 +1,18 @@ -function plugindef() +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,11 +40,11 @@ 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." + return t.menu, t.menu, t.desc end local transposition = require("library.transposition") +local loc = require('library.localization') function transpose_enharmonic_up() local success = true @@ -40,7 +54,7 @@ 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("error_msg_transposition"), loc.localize("transposition_error")) end end diff --git a/utilities/localization_tool.lua b/utilities/localization_tool.lua new file mode 100644 index 00000000..73962606 --- /dev/null +++ b/utilities/localization_tool.lua @@ -0,0 +1,556 @@ +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 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. + + Users of this script will get the best results if they use it in tandem with an indegrated development + 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 + +-- 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 osutils = require("luaosutils") +local https = osutils.internet + +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)() + +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", + ["English"] = "en", + ["German"] = "de", + ["French"] = "fr", + ["Italian"] = "it", + ["Japanese"] = "ja", + ["Polish"] = "pl", + ["Spanish"] = "es", + ["Swedish"] = "sv" +} + +--[[ +% 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", + ... -- 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 +]] +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") + if file then + 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 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 + end + 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) + local concat = {} + 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, tab_str .. generate_key_candidate(k) .. " = \"" .. tostring(v) .. "\",\n") + end + 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 + +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 + +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. The base table does not need to be saved +as a localization. + +@ file_path (string) the file_path to search for strings. +]] +local function create_localized_base_table_string(file_path) + local t = create_localized_base_table(file_path) + 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) + -- 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 = 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) + 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.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 + end + index = index + 1 + end + table.insert(concat, tab_str .. "}") + 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) + 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) + -- 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 + ]] .. "Here is Lua source code for a table of keys and values:\n\n```\n" .. table_string .. "\n```\n" .. + [[ + 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. + ]] + https_session = openai.create_completion("gpt-4", prompt, 0.2, callback) + set_enable_all() +end + +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_script_open +local function on_generate(control) + local popup = global_dialog:GetControl("file_list") + if popup:GetCount() <= 0 then + on_script_open(control) + end + if popup:GetCount() > 0 then + local sel_item = popup:GetSelectedItem() + create_localized_base_table_string(popup:GetItemText(sel_item)) + end + global_dialog:GetControl("editor"):SetKeyboardFocus() +end + +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)) + 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") + 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 + 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 + +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)" + end + 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, + 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.", "") + text_copied = true + end + end + if not text_copied then + mixin.UI():AlertError("No plugindef function found.", "") + end + 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") + 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: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) + dlg:CreateComboBox(0, curr_y, "lang_list") + :DoAutoResizeWidth(0) + :AddStrings(utils.create_keys_table(finale_supported_languages)) + :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)) + 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) + :HorizontallyAlignRightWith(dlg:GetControl("lang_list"), utils.win_mac(0, -3)) + 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(editor) + -- registrations + dlg:RegisterCloseWindow(on_close) + -- return + return dlg +end + +local function localization_tool() + global_dialog = global_dialog or create_dialog() + global_dialog:RunModeless() +end + +localization_tool()