From 40efe0b0c23b5a2cce6b09c45b69456a56b34213 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Thu, 8 Aug 2024 20:10:43 -0700 Subject: [PATCH 01/12] Taking another shot at i18n Co-Authored-By: Meadowsys <49562048+meadowsys@users.noreply.github.com> --- package.json | 1 + src/atom-environment.js | 6 ++ src/config-schema.js | 22 +++++ src/i18n.js | 187 ++++++++++++++++++++++++++++++++++++++++ yarn.lock | 54 ++++++++++++ 5 files changed, 270 insertions(+) create mode 100644 src/i18n.js diff --git a/package.json b/package.json index 17339a8718..683ec6039d 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "grim": "2.0.3", "image-view": "file:packages/image-view", "incompatible-packages": "file:packages/incompatible-packages", + "intl-messageformat": "^10.5.14", "jasmine-json": "~0.0", "jasmine-reporters": "1.1.0", "jasmine-tagged": "^1.1.4", diff --git a/src/atom-environment.js b/src/atom-environment.js index acdd901edc..58ed1b88e5 100644 --- a/src/atom-environment.js +++ b/src/atom-environment.js @@ -47,6 +47,7 @@ const TextEditorRegistry = require('./text-editor-registry'); const StartupTime = require('./startup-time'); const { getReleaseChannel } = require('./get-app-details.js'); const UI = require('./ui.js'); +const I18n = require("./i18n.js"); const packagejson = require("../package.json"); const stat = util.promisify(fs.stat); @@ -219,6 +220,9 @@ class AtomEnvironment { stateStore: this.stateStore }); + /** @type {I18n} */ + this.i18n = new I18n(); + this.branding = { id: packagejson.branding.id, name: packagejson.branding.name, @@ -275,6 +279,8 @@ class AtomEnvironment { this.project.replace(projectSpecification); } + this.i18n.initialize(); + this.menu.initialize({ resourcePath }); this.contextMenu.initialize({ resourcePath, devMode }); diff --git a/src/config-schema.js b/src/config-schema.js index d1a2ed2fe4..b8c0879e30 100644 --- a/src/config-schema.js +++ b/src/config-schema.js @@ -410,6 +410,28 @@ const configSchema = { default: false, title: 'Allow Window Transparency', description: `Allows editor windows to be see-through. When this setting is enabled, UI themes and user stylesheets can use background colors with an alpha channel to make editor windows translucent. Takes effect after a restart of Pulsar.` + }, + language: { + type: "object", + description: "Language and Locale options. Requires a restart of Pulsar to take effect.", + properties: { + primary: { + type: "string", + order: 1, + default: "en-US", + description: "The primary language/locale you prefer." + }, + priorityList: { + type: "array", + order: 2, + description: "List of alternative languages to load. Highest priority to lowest. Use the most specific locale selectors possible.", + default: [], + items: { + type: "string", + // TODO consider enum options, maybe? + } + } + } } } }, diff --git a/src/i18n.js b/src/i18n.js new file mode 100644 index 0000000000..4d8fb58d44 --- /dev/null +++ b/src/i18n.js @@ -0,0 +1,187 @@ +const keyPathHelpers = require("key-path-helpers"); +const IntlMessageFormat = require("intl-messageformat").default; + +// Truncate trailing subtag, as well as single letter or digit subtags +// which introduce private-use sequences, and extensions, and are not valid +// locale selectors alone. +const LOCALE_LOOKUP_FILTER_TRUNCATE = /(-\w){0,1}-[\w*]*?$/g; + +function generateLocaleFallback(list, lang) { + // Breakdown a lang provided into the fallback options + let locale = lang; + while(locale.length > 0) { + if (!locale.includes("-")) { + // If we have no more subtags to remove + locale = ""; + break; + } + locale = locale.replace(LOCALE_LOOKUP_FILTER_TRUNCATE, ""); + list.add(locale); + } + return; +} + +module.exports = +class I18n { + // Provides LocaleNegotiation in accordance to RFC4647 Lookup Filtering Fallback Pattern + // Provided a priorityList, primary language, and default language. + static LocaleNegotiation( + primary, + priorityList, + fallback = "en" // Hardcoding to ensure we can always fallback to something + ) { + // First create the lookup list + const lookupList = new Set(); + + if (typeof primary === "string") { + // The first entry should be the primary language + lookupList.add(primary); + // Provide fallback options from the primary language + generateLocaleFallback(lookupList, primary); + } + + if (Array.isArray(priorityList)) { + for (const lang of priorityList) { + // Since the first entry should be fully formed, we will add it directly + lookupList.add(lang); + // Then we breakdown the lang provided into the fallback options + generateLocaleFallback(lookupList, lang); + } + } + + // After adding all items in the priority list, lets add the default + lookupList.add(fallback); + + return lookupList; + } + + // Determines if the provided locale should be loaded. Based on the user's + // current settings. + static ShouldIncludeLocale(locale, primary, priorityList, fallback) { + const localeList = I18n.LocaleNegotiation(primary, priorityList, fallback); + + for (const localeListItem of localeList) { + if (I18n.DoLocalesMatch(localeListItem, locale)) { + return true; + } + } + + return false; + } + + // Takes a wanted locale, and the locale you have, to determine if they match + static DoLocalesMatch(want, have) { + if (want == have) { + return true; + } + if (want.endsWith("-*") && have.includes("-")) { + // Attempt to match with wildcard + let wantArr = want.split("-"); + let haveArr = have.split("-"); + for (let i = 0; i < wantArr.length; i++) { + if (wantArr[i] == "*") { + // wants contains a wildcard match, doesn't matter what the have is + return true; + } else if (wantArr[i] != haveArr[i]) { + return false; + } // else they equal, and we let the loop continue to check the next place + } + } else { + // As we don't do any fallback behavior here, we can safely say these do + // not match. + return false; + } + } + + constructor() { + this.strings = {}; + this.localeFallbackList = null; + } + + // Helps along with initial setup + initialize() { + this.localeFallbackList = I18n.LocaleNegotiation( + atom.config.get("core.language.primary"), + atom.config.get("core.language.priorityList") + ); + + // Maybe add loading of internal locales? + } + + addStrings(newObj, locale, stringObj = this.strings) { + if (typeof newObj === "object") { + for (const key in newObj) { + if (typeof stringObj[key] !== "object") { + // We only want to initialize an empty object, if this doesn't + // already exist on the string tree + stringObj[key] = {}; + } + stringObj[key] = this.addStrings(newObj[key], locale, stringObj[key]); + } + } else if (typeof newObj === "string") { + // We have hit the final entry in the object, and it is that of a string + // Meaning this value is the translated string itself, so we will add it + // within a final key of the locale + stringObj[locale] = newObj; + } + return stringObj; + } + + addString(keyPath, str, locale) { + keyPathHelpers.setValueAtKeyPath(this.strings, `${keyPath}.${locale}`, str); + } + + t(keyPath, opts) { + return this.translate(keyPath, opts); + } + + translate(keyPath, opts = {}) { + const stringLocales = keyPathHelpers.getValueAtKeyPath(this.strings, keyPath); + let bestLocale; + + if (this.localeFallbackList == null) { + this.localeFallbackList = I18n.LocaleNegotiation( + atom.config.get("core.language.primary"), + atom.config.get("core.language.priorityList") + ); + } + + // Find the first match for a locale available within the string + localeFallbackListLoop: + for (const localeListItem of this.localeFallbackList) { + for (const possibleLocale in stringLocales) { + if (I18n.DoLocalesMatch(localeListItem, possibleLocale)) { + bestLocale = possibleLocale; + break localeFallbackListLoop; + } + } + } + + const msg = new IntlMessageFormat(stringLocales[bestLocale], bestLocale); + + return msg.format(opts); + } + + getT(namespace) { + return this.getTranslate(namespace); + } + + getTranslate(namespace) { + return new NamespaceI18n(namespace, this); + } +} + +class NamespaceI18n { + constructor(namespace, i18n) { + this.namespace = namespace; + this.i18n = i18n; + } + + t(keyPath, opts) { + return this.translate(keyPath, opts); + } + + translate(keyPath, opts) { + return this.i18n.translate(`${this.namespace}.${keyPath}`, opts); + } +} diff --git a/yarn.lock b/yarn.lock index 4505c010e9..b83d750410 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1431,6 +1431,45 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" +"@formatjs/ecma402-abstract@2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-2.0.0.tgz#39197ab90b1c78b7342b129a56a7acdb8f512e17" + integrity sha512-rRqXOqdFmk7RYvj4khklyqzcfQl9vEL/usogncBHRZfZBDOwMGuSRNFl02fu5KGHXdbinju+YXyuR+Nk8xlr/g== + dependencies: + "@formatjs/intl-localematcher" "0.5.4" + tslib "^2.4.0" + +"@formatjs/fast-memoize@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-2.2.0.tgz#33bd616d2e486c3e8ef4e68c99648c196887802b" + integrity sha512-hnk/nY8FyrL5YxwP9e4r9dqeM6cAbo8PeU9UjyXojZMNvVad2Z06FAVHyR3Ecw6fza+0GH7vdJgiKIVXTMbSBA== + dependencies: + tslib "^2.4.0" + +"@formatjs/icu-messageformat-parser@2.7.8": + version "2.7.8" + resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.7.8.tgz#f6d7643001e9bb5930d812f1f9a9856f30fa0343" + integrity sha512-nBZJYmhpcSX0WeJ5SDYUkZ42AgR3xiyhNCsQweFx3cz/ULJjym8bHAzWKvG5e2+1XO98dBYC0fWeeAECAVSwLA== + dependencies: + "@formatjs/ecma402-abstract" "2.0.0" + "@formatjs/icu-skeleton-parser" "1.8.2" + tslib "^2.4.0" + +"@formatjs/icu-skeleton-parser@1.8.2": + version "1.8.2" + resolved "https://registry.yarnpkg.com/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.2.tgz#2252c949ae84ee66930e726130ea66731a123c9f" + integrity sha512-k4ERKgw7aKGWJZgTarIcNEmvyTVD9FYh0mTrrBMHZ1b8hUu6iOJ4SzsZlo3UNAvHYa+PnvntIwRPt1/vy4nA9Q== + dependencies: + "@formatjs/ecma402-abstract" "2.0.0" + tslib "^2.4.0" + +"@formatjs/intl-localematcher@0.5.4": + version "0.5.4" + resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz#caa71f2e40d93e37d58be35cfffe57865f2b366f" + integrity sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g== + dependencies: + tslib "^2.4.0" + "@gar/promisify@^1.0.1": version "1.1.3" resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" @@ -5453,6 +5492,16 @@ interpret@^1.0.0: resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== +intl-messageformat@^10.5.14: + version "10.5.14" + resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-10.5.14.tgz#e5bb373f8a37b88fbe647d7b941f3ab2a37ed00a" + integrity sha512-IjC6sI0X7YRjjyVH9aUgdftcmZK7WXdHeil4KwbjDnRWjnVitKpAx3rr6t6di1joFp5188VqKcobOPA6mCLG/w== + dependencies: + "@formatjs/ecma402-abstract" "2.0.0" + "@formatjs/fast-memoize" "2.2.0" + "@formatjs/icu-messageformat-parser" "2.7.8" + tslib "^2.4.0" + invert-kv@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" @@ -9472,6 +9521,11 @@ tslib@^2.3.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.0.tgz#b295854684dbda164e181d259a22cd779dcd7bc3" integrity sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA== +tslib@^2.4.0: + version "2.6.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== + tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" From 7449b4487a4a85bad423e37a485e70e461a39d39 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Thu, 8 Aug 2024 20:38:56 -0700 Subject: [PATCH 02/12] Load core locales during initialization Co-Authored-By: Meadowsys <49562048+meadowsys@users.noreply.github.com> --- src/atom-environment.js | 5 ++++- src/i18n.js | 22 ++++++++++++++++++---- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/atom-environment.js b/src/atom-environment.js index 58ed1b88e5..a7834fcba9 100644 --- a/src/atom-environment.js +++ b/src/atom-environment.js @@ -279,7 +279,10 @@ class AtomEnvironment { this.project.replace(projectSpecification); } - this.i18n.initialize(); + this.i18n.initialize({ + resourcePath: resourcePath, + config: this.config + }); this.menu.initialize({ resourcePath }); this.contextMenu.initialize({ resourcePath, devMode }); diff --git a/src/i18n.js b/src/i18n.js index 4d8fb58d44..ea7e731b9a 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -1,3 +1,6 @@ +const path = require("path"); +const fs = require("fs-plus"); +const CSON = require("season"); const keyPathHelpers = require("key-path-helpers"); const IntlMessageFormat = require("intl-messageformat").default; @@ -99,13 +102,24 @@ class I18n { } // Helps along with initial setup - initialize() { + initialize({ resourcePath, config }) { this.localeFallbackList = I18n.LocaleNegotiation( - atom.config.get("core.language.primary"), - atom.config.get("core.language.priorityList") + config.get("core.language.primary"), + config.get("core.language.priorityList") ); - // Maybe add loading of internal locales? + // Load Pulsar Core Locales + const localesPath = path.join(resourcePath, "locales"); + const localesPaths = fs.listSync(localesPath, ["cson", "json"]); + + for (const localePath of localesPaths) { + const localeFilePath = localePath.split("."); + // `pulsar.en-US.json` => `en-US` + const locale = localeFilePath[localeFilePath.length - 2] ?? ""; + if (I18n.ShouldIncludeLocale(locale)) { + this.addStrings(CSON.readFileSync(localePath) || {}, locale); + } + } } addStrings(newObj, locale, stringObj = this.strings) { From 9b9094c71a8914a6fa225074fc0decbcb9b4b39c Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Fri, 9 Aug 2024 00:12:34 -0700 Subject: [PATCH 03/12] UI translations, context-menu translations Co-Authored-By: Meadowsys <49562048+meadowsys@users.noreply.github.com> --- locales/pulsar.en.cson | 7 +++++++ menus/win32.cson | 2 +- .../lib/command-palette-view.js | 4 ++-- .../locales/command-palette.en.cson | 4 ++++ src/atom-environment.js | 6 +++--- src/context-menu-manager.js | 12 +++++++++++ src/i18n.js | 15 +++++++++++++ src/package.js | 21 +++++++++++++++++++ 8 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 locales/pulsar.en.cson create mode 100644 packages/command-palette/locales/command-palette.en.cson diff --git a/locales/pulsar.en.cson b/locales/pulsar.en.cson new file mode 100644 index 0000000000..96fe1b76da --- /dev/null +++ b/locales/pulsar.en.cson @@ -0,0 +1,7 @@ +'core': { + 'welcomeString': 'Hello Pulsar!' + 'goodbyeString': 'Goodbye Pulsar!' +} +'context-menu': { + 'core:undo': 'Undo... or something I guess' +} diff --git a/menus/win32.cson b/menus/win32.cson index 17ae6df142..bd6af4e760 100644 --- a/menus/win32.cson +++ b/menus/win32.cson @@ -209,7 +209,7 @@ 'context-menu': 'atom-text-editor, .overlayer': [ - {label: 'Undo', command: 'core:undo'} + {label: '%context-menu.core:undo%', command: 'core:undo'} {label: 'Redo', command: 'core:redo'} {type: 'separator'} {label: 'Cut', command: 'core:cut'} diff --git a/packages/command-palette/lib/command-palette-view.js b/packages/command-palette/lib/command-palette-view.js index 40703d1487..bc8dabf7ab 100644 --- a/packages/command-palette/lib/command-palette-view.js +++ b/packages/command-palette/lib/command-palette-view.js @@ -10,7 +10,7 @@ export default class CommandPaletteView { initiallyVisibleItemCount: initiallyVisibleItemCount, // just for being able to disable visible-on-render in spec items: [], filter: this.filter, - emptyMessage: 'No matches found', + emptyMessage: atom.i18n.t("command-palette.commandPaletteView.emptyMessage"), elementForItem: (item, {index, selected, visible}) => { if (!visible) { return document.createElement("li") @@ -232,7 +232,7 @@ export default class CommandPaletteView { }) const introEl = document.createElement('strong') - introEl.textContent = 'matching tags: ' + introEl.textContent = atom.i18n.t("command-palette.commandPaletteView.matchingTags"); tagsEl.appendChild(introEl) matchingTags.map(t => this.createTag(t, query)).forEach((tagEl, i) => { diff --git a/packages/command-palette/locales/command-palette.en.cson b/packages/command-palette/locales/command-palette.en.cson new file mode 100644 index 0000000000..5637eda166 --- /dev/null +++ b/packages/command-palette/locales/command-palette.en.cson @@ -0,0 +1,4 @@ +'commandPaletteView': { + 'emptyMessage': 'No matches found' + 'matchingTags': 'matching tags: ' +} diff --git a/src/atom-environment.js b/src/atom-environment.js index a7834fcba9..0b346cebc5 100644 --- a/src/atom-environment.js +++ b/src/atom-environment.js @@ -127,6 +127,9 @@ class AtomEnvironment { /** @type {StyleManager} */ this.styles = new StyleManager(); + /** @type {I18n} */ + this.i18n = new I18n(); + /** @type {PackageManager} */ this.packages = new PackageManager({ config: this.config, @@ -220,9 +223,6 @@ class AtomEnvironment { stateStore: this.stateStore }); - /** @type {I18n} */ - this.i18n = new I18n(); - this.branding = { id: packagejson.branding.id, name: packagejson.branding.name, diff --git a/src/context-menu-manager.js b/src/context-menu-manager.js index 10aad2877d..b2aff6a627 100644 --- a/src/context-menu-manager.js +++ b/src/context-menu-manager.js @@ -277,6 +277,18 @@ var ContextMenuItemSet = class ContextMenuItemSet { this.selector = selector1; this.items = items1; this.specificity = calculateSpecificity(this.selector); + + this.localize(); + } + + localize() { + for (const item of this.items) { + if (/^%.+%$/.test(item.label)) { + // The item label begins and ends with `%` meaning it's a locale + // key that should be auto-translated + item.label = atom.i18n.t(item.label.replace(/%/g, "")) ?? item.label; + } + } } }; diff --git a/src/i18n.js b/src/i18n.js index ea7e731b9a..f0a77f97dd 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -122,6 +122,10 @@ class I18n { } } + shouldIncludeLocale(locale) { + return I18n.ShouldIncludeLocale(locale); + } + addStrings(newObj, locale, stringObj = this.strings) { if (typeof newObj === "object") { for (const key in newObj) { @@ -151,6 +155,12 @@ class I18n { translate(keyPath, opts = {}) { const stringLocales = keyPathHelpers.getValueAtKeyPath(this.strings, keyPath); + + if (typeof stringLocales !== "object") { + // If the keypath requested doesn't exist, return null + return null; + } + let bestLocale; if (this.localeFallbackList == null) { @@ -171,6 +181,11 @@ class I18n { } } + if (!stringLocales[bestLocale]) { + // If we couldn't find any way to read the string, return null + return null; + } + const msg = new IntlMessageFormat(stringLocales[bestLocale], bestLocale); return msg.format(opts); diff --git a/src/package.js b/src/package.js index 1831e919d9..370b1bee2c 100644 --- a/src/package.js +++ b/src/package.js @@ -95,6 +95,7 @@ module.exports = class Package { preload() { this.loadKeymaps(); this.loadMenus(); + this.loadLocales(); this.registerDeserializerMethods(); this.activateCoreStartupServices(); this.registerURIHandler(); @@ -130,6 +131,7 @@ module.exports = class Package { this.loadKeymaps(); this.loadMenus(); + this.loadLocales(); this.loadStylesheets(); this.registerDeserializerMethods(); this.activateCoreStartupServices(); @@ -531,6 +533,25 @@ module.exports = class Package { } } + loadLocales() { + const localesDirPath = path.join(this.path, "locales"); + const localesPaths = fs.listSync(localesDirPath, ["cson", "json"]); + + for (const localePath of localesPaths) { + const localeFilePath = localePath.split("."); + // `package-name.en-US.json` => `en-US` + const locale = localeFilePath[localeFilePath.length - 2] ?? ""; + if (atom.i18n.shouldIncludeLocale(locale)) { + const localeFile = CSON.readFileSync(localePath); + if (localeFile) { + const localeObj = {}; + localeObj[this.name] = localeFile; + atom.i18n.addStrings(localeObj, locale); + } + } + } + } + getKeymapPaths() { const keymapsDirPath = path.join(this.path, 'keymaps'); if (this.metadata.keymaps) { From 9f47be8ac9d8978bf4fbd5199144cba872923c68 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Fri, 9 Aug 2024 00:38:26 -0700 Subject: [PATCH 04/12] Menu item translations, cleanup auto-translations Co-Authored-By: Meadowsys <49562048+meadowsys@users.noreply.github.com> --- locales/pulsar.en.cson | 5 ++++- menus/win32.cson | 2 +- src/atom-environment.js | 2 +- src/context-menu-manager.js | 14 +++++++------- src/i18n.js | 14 ++++++++++++++ src/menu-manager.js | 13 +++++++++++++ 6 files changed, 40 insertions(+), 10 deletions(-) diff --git a/locales/pulsar.en.cson b/locales/pulsar.en.cson index 96fe1b76da..0294d4b2f7 100644 --- a/locales/pulsar.en.cson +++ b/locales/pulsar.en.cson @@ -2,6 +2,9 @@ 'welcomeString': 'Hello Pulsar!' 'goodbyeString': 'Goodbye Pulsar!' } +'menu': { + 'settings-view:view-installed-packages': 'Open Package Manager' +} 'context-menu': { - 'core:undo': 'Undo... or something I guess' + 'core:undo': 'Undo' } diff --git a/menus/win32.cson b/menus/win32.cson index bd6af4e760..ffd1c763d7 100644 --- a/menus/win32.cson +++ b/menus/win32.cson @@ -178,7 +178,7 @@ { label: '&Packages' submenu: [ - { label: 'Open Package Manager', command: 'settings-view:view-installed-packages' } + { label: '%menu.settings-view:view-installed-packages%', command: 'settings-view:view-installed-packages' } { type: 'separator' } ] } diff --git a/src/atom-environment.js b/src/atom-environment.js index 0b346cebc5..ffc4a6fc55 100644 --- a/src/atom-environment.js +++ b/src/atom-environment.js @@ -160,7 +160,7 @@ class AtomEnvironment { }); /** @type {ContextMenuManager} */ - this.contextMenu = new ContextMenuManager({ keymapManager: this.keymaps }); + this.contextMenu = new ContextMenuManager({ keymapManager: this.keymaps, i18n: this.i18n }); this.packages.setMenuManager(this.menu); this.packages.setContextMenuManager(this.contextMenu); diff --git a/src/context-menu-manager.js b/src/context-menu-manager.js index b2aff6a627..e58dd4b870 100644 --- a/src/context-menu-manager.js +++ b/src/context-menu-manager.js @@ -45,8 +45,9 @@ if (buildMetadata != null && buildMetadata._atomMenu != null && buildMetadata._a // The format for use in {::add} is the same minus the `context-menu` key. See // {::add} for more information. module.exports = class ContextMenuManager { - constructor({keymapManager}) { + constructor({keymapManager, i18n}) { this.keymapManager = keymapManager; + this.i18n = i18n; this.definitions = { '.overlayer': [] // TODO: Remove once color picker package stops touching private data }; @@ -130,7 +131,7 @@ module.exports = class ContextMenuManager { if (throwOnInvalidSelector) { validateSelector(selector); } - const itemSet = new ContextMenuItemSet(selector, items); + const itemSet = new ContextMenuItemSet(selector, items, this.i18n); addedItemSets.push(itemSet); this.itemSets.push(itemSet); } @@ -273,20 +274,19 @@ module.exports = class ContextMenuManager { }; var ContextMenuItemSet = class ContextMenuItemSet { - constructor(selector1, items1) { + constructor(selector1, items1, i18n) { this.selector = selector1; this.items = items1; this.specificity = calculateSpecificity(this.selector); + this.i18n = i18n; this.localize(); } localize() { for (const item of this.items) { - if (/^%.+%$/.test(item.label)) { - // The item label begins and ends with `%` meaning it's a locale - // key that should be auto-translated - item.label = atom.i18n.t(item.label.replace(/%/g, "")) ?? item.label; + if (this.i18n.isAutoTranslateLabel(item.label)) { + item.label = this.i18n.translateLabel(item.label); } } } diff --git a/src/i18n.js b/src/i18n.js index f0a77f97dd..92bded8e92 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -9,6 +9,8 @@ const IntlMessageFormat = require("intl-messageformat").default; // locale selectors alone. const LOCALE_LOOKUP_FILTER_TRUNCATE = /(-\w){0,1}-[\w*]*?$/g; +const AUTO_TRANSLATE_LABEL = /^%.+%$/; + function generateLocaleFallback(list, lang) { // Breakdown a lang provided into the fallback options let locale = lang; @@ -198,6 +200,18 @@ class I18n { getTranslate(namespace) { return new NamespaceI18n(namespace, this); } + + // === Helper Methods + isAutoTranslateLabel(value) { + return AUTO_TRANSLATE_LABEL.test(value); + } + + // Used in the menu and context-menu when auto-translating labels + translateLabel(label) { + // Since failing to translate menus could crash Pulsar + // We must ensure to fallback to the raw label value + return this.translate(label.replace(/%/g, "")) ?? label; + } } class NamespaceI18n { diff --git a/src/menu-manager.js b/src/menu-manager.js index c67e338976..0db22827d5 100644 --- a/src/menu-manager.js +++ b/src/menu-manager.js @@ -106,6 +106,19 @@ module.exports = MenuManager = class MenuManager { if (item.label == null) { continue; // TODO: Should we emit a warning here? } + + // Localize item label + if (atom.i18n.isAutoTranslateLabel(item.label)) { + item.label = atom.i18n.translateLabel(item.label); + } + if (Array.isArray(item.submenu)) { + for (let y = 0; y < item.submenu.length; y++) { + if (atom.i18n.isAutoTranslateLabel(item.submenu[y].label)) { + item.submenu[y].label = atom.i18n.translateLabel(item.submenu[y].label); + } + } + } + this.merge(this.template, item); } this.update(); From 5c3b47aa4bff3d5d90eef4e6db757b8d68eda852 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Fri, 9 Aug 2024 01:47:23 -0700 Subject: [PATCH 05/12] Stop using `atom` global Because the `atom` global doesn't seem available during test runs Co-Authored-By: Meadowsys <49562048+meadowsys@users.noreply.github.com> --- src/atom-environment.js | 10 ++++++---- src/i18n.js | 13 +++++++------ src/menu-manager.js | 11 ++++++----- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/atom-environment.js b/src/atom-environment.js index ffc4a6fc55..2c731aeb64 100644 --- a/src/atom-environment.js +++ b/src/atom-environment.js @@ -128,7 +128,9 @@ class AtomEnvironment { this.styles = new StyleManager(); /** @type {I18n} */ - this.i18n = new I18n(); + this.i18n = new I18n({ + config: this.config + }); /** @type {PackageManager} */ this.packages = new PackageManager({ @@ -156,7 +158,8 @@ class AtomEnvironment { /** @type {MenuManager} */ this.menu = new MenuManager({ keymapManager: this.keymaps, - packageManager: this.packages + packageManager: this.packages, + i18n: this.i18n }); /** @type {ContextMenuManager} */ @@ -280,8 +283,7 @@ class AtomEnvironment { } this.i18n.initialize({ - resourcePath: resourcePath, - config: this.config + resourcePath: resourcePath }); this.menu.initialize({ resourcePath }); diff --git a/src/i18n.js b/src/i18n.js index 92bded8e92..744bd1533a 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -98,16 +98,17 @@ class I18n { } } - constructor() { + constructor({ config }) { + this.config = config; this.strings = {}; this.localeFallbackList = null; } // Helps along with initial setup - initialize({ resourcePath, config }) { + initialize({ resourcePath }) { this.localeFallbackList = I18n.LocaleNegotiation( - config.get("core.language.primary"), - config.get("core.language.priorityList") + this.config.get("core.language.primary"), + this.config.get("core.language.priorityList") ); // Load Pulsar Core Locales @@ -167,8 +168,8 @@ class I18n { if (this.localeFallbackList == null) { this.localeFallbackList = I18n.LocaleNegotiation( - atom.config.get("core.language.primary"), - atom.config.get("core.language.priorityList") + this.config.get("core.language.primary"), + this.config.get("core.language.priorityList") ); } diff --git a/src/menu-manager.js b/src/menu-manager.js index 0db22827d5..f2e649d65e 100644 --- a/src/menu-manager.js +++ b/src/menu-manager.js @@ -60,10 +60,11 @@ if (buildMetadata) { // // See {::add} for more info about adding menu's directly. module.exports = MenuManager = class MenuManager { - constructor({resourcePath, keymapManager, packageManager}) { + constructor({resourcePath, keymapManager, packageManager, i18n}) { this.resourcePath = resourcePath; this.keymapManager = keymapManager; this.packageManager = packageManager; + this.i18n = i18n; this.initialized = false; this.pendingUpdateOperation = null; this.template = []; @@ -108,13 +109,13 @@ module.exports = MenuManager = class MenuManager { } // Localize item label - if (atom.i18n.isAutoTranslateLabel(item.label)) { - item.label = atom.i18n.translateLabel(item.label); + if (this.i18n.isAutoTranslateLabel(item.label)) { + item.label = this.i18n.translateLabel(item.label); } if (Array.isArray(item.submenu)) { for (let y = 0; y < item.submenu.length; y++) { - if (atom.i18n.isAutoTranslateLabel(item.submenu[y].label)) { - item.submenu[y].label = atom.i18n.translateLabel(item.submenu[y].label); + if (this.i18n.isAutoTranslateLabel(item.submenu[y].label)) { + item.submenu[y].label = this.i18n.translateLabel(item.submenu[y].label); } } } From 431d8024204fb8cdee922d5d515006ab034a3e8a Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Fri, 9 Aug 2024 02:22:39 -0700 Subject: [PATCH 06/12] Update specs to ensure they pass Co-Authored-By: Meadowsys <49562048+meadowsys@users.noreply.github.com> --- spec/context-menu-manager-spec.js | 2 +- spec/menu-manager-spec.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/spec/context-menu-manager-spec.js b/spec/context-menu-manager-spec.js index e0246ee018..2b2e92b657 100644 --- a/spec/context-menu-manager-spec.js +++ b/spec/context-menu-manager-spec.js @@ -5,7 +5,7 @@ describe('ContextMenuManager', function() { beforeEach(function() { const { resourcePath } = atom.getLoadSettings(); - contextMenu = new ContextMenuManager({ keymapManager: atom.keymaps }); + contextMenu = new ContextMenuManager({ keymapManager: atom.keymaps, i18n: atom.i18n }); contextMenu.initialize({ resourcePath }); parent = document.createElement('div'); diff --git a/spec/menu-manager-spec.js b/spec/menu-manager-spec.js index 3b78e42b60..ea9cb5af70 100644 --- a/spec/menu-manager-spec.js +++ b/spec/menu-manager-spec.js @@ -7,7 +7,8 @@ describe('MenuManager', function() { beforeEach(function() { menu = new MenuManager({ keymapManager: atom.keymaps, - packageManager: atom.packages + packageManager: atom.packages, + i18n: atom.i18n }); spyOn(menu, 'sendToBrowserProcess'); // Do not modify Atom's actual menus menu.initialize({ resourcePath: atom.getLoadSettings().resourcePath }); From d73cd15e5223ae0dea49827df6fd142b921b8789 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Fri, 9 Aug 2024 02:37:14 -0700 Subject: [PATCH 07/12] Localize: Settings Title & Description Co-Authored-By: Meadowsys <49562048+meadowsys@users.noreply.github.com> --- locales/pulsar.en.cson | 5 +++++ packages/settings-view/lib/rich-description.js | 6 ++++++ packages/settings-view/lib/rich-title.js | 6 +++++- src/config-schema.js | 2 +- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/locales/pulsar.en.cson b/locales/pulsar.en.cson index 0294d4b2f7..f187576377 100644 --- a/locales/pulsar.en.cson +++ b/locales/pulsar.en.cson @@ -2,6 +2,11 @@ 'welcomeString': 'Hello Pulsar!' 'goodbyeString': 'Goodbye Pulsar!' } +'config': { + 'excludeVcsIgnoredPaths': { + 'title': 'Exclude VCS Ignored Paths' + } +} 'menu': { 'settings-view:view-installed-packages': 'Open Package Manager' } diff --git a/packages/settings-view/lib/rich-description.js b/packages/settings-view/lib/rich-description.js index a71f43cb63..2bbb153e17 100644 --- a/packages/settings-view/lib/rich-description.js +++ b/packages/settings-view/lib/rich-description.js @@ -6,6 +6,12 @@ module.exports = { if (schema && schema.description) { description = schema.description } + + // Localize + if (atom.i18n.isAutoTranslateLabel(description)) { + description = atom.i18n.translateLabel(description); + } + return atom.ui.markdown.render( description, { diff --git a/packages/settings-view/lib/rich-title.js b/packages/settings-view/lib/rich-title.js index 7b50e59866..72f39c86ec 100644 --- a/packages/settings-view/lib/rich-title.js +++ b/packages/settings-view/lib/rich-title.js @@ -6,7 +6,11 @@ module.exports = { name = '' } const schema = atom.config.getSchema(keyPath) - const title = schema != null ? schema.title : null + let title = schema != null ? schema.title : null + // Localize + if (typeof title === "string" && atom.i18n.isAutoTranslateLabel(title)) { + title = atom.i18n.translateLabel(title); + } return title || _.uncamelcase(name).split('.').map(_.capitalize).join(' ') } } diff --git a/src/config-schema.js b/src/config-schema.js index b8c0879e30..7475700a6e 100644 --- a/src/config-schema.js +++ b/src/config-schema.js @@ -25,7 +25,7 @@ const configSchema = { excludeVcsIgnoredPaths: { type: 'boolean', default: true, - title: 'Exclude VCS Ignored Paths', + title: '%config.excludeVcsIgnoredPaths.title%', description: "Files and directories ignored by the current project's VCS will be ignored by some packages, such as the fuzzy finder and find and replace. For example, projects using Git have these paths defined in the .gitignore file. Individual packages might have additional config settings for ignoring VCS ignored files and folders." }, From b7bd819f6868ba9648da7365fbded682dfe0e089 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Fri, 9 Aug 2024 18:02:25 -0700 Subject: [PATCH 08/12] Add tests --- spec/i18n-spec.js | 241 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 spec/i18n-spec.js diff --git a/spec/i18n-spec.js b/spec/i18n-spec.js new file mode 100644 index 0000000000..f87dcf3439 --- /dev/null +++ b/spec/i18n-spec.js @@ -0,0 +1,241 @@ +const I18n = require("../src/i18n.js"); + +describe("I18n", () => { + let i18n; + + beforeEach(() => { + const { resourcePath } = atom.getLoadSettings(); + i18n = new I18n({ + config: atom.config + }); + i18n.initialize({ resourcePath }); + }); + + describe("Handles Locales logic", () => { + it("Locale Lookup Filtering Fallback Pattern Array: Follows RFC4647", () => { + const primaryLocale = "es-MX"; + const priorityListLocale = [ + "zh-Hant-CN-x-private1-private2", + "en-US" + ]; + const fallbackArray = I18n.LocaleNegotiation(primaryLocale, priorityListLocale); + + expect(fallbackArray).toEqual([ + "es-MX", + "es", + "zh-Hant-CN-x-private1-private2", + "zh-Hant-CN-x-private1", + "zh-Hant-CN", + "zh-Hant", + "zh", + "en-US", + "en" + ]); + }); + + it("Locale Lookup Filtering Fallback Pattern Array: Excludes duplicates from hardcoded fallback", () => { + const primaryLocale = "en-US"; + const priorityListLocale = [ "es-MX" ]; + const fallbackArray = I18n.LocaleNegotiation(primaryLocale, priorityListLocale); + + expect(fallbackArray).toEqual([ + "en-US", + "en", + "es-MX", + "es" + ]); + }); + + it("Accurately determines if Locale Should be included when it should", () => { + const primaryLocale = "en-US"; + const priorityListLocale = [ "es-MX" ]; + const questionedLocale = "en"; + const shouldBeIncluded = I18n.ShouldIncludeLocale(questionedLocale, primaryLocale, priorityListLocale); + expect(shouldBeIncluded).toEqual(true); + }); + + it("Accurately determines if Locale Should be included when it shoudln't", () => { + const primaryLocale = "zh-Hant"; + const priorityListLocale = [ "es-MX" ]; + const questionedLocale = "ja-JP"; + const shouldBeIncluded = I18n.ShouldIncludeLocale(questionedLocale, primaryLocale, priorityListLocale); + expect(shouldBeIncluded).toEqual(false); + }); + + it("Accurately determines if Locale should be included when the locale is the hardcoded fallback", () => { + const primaryLocale = "zh-Hant"; + const priorityListLocale = [ "es-MX" ]; + const questionedLocale = "en"; + const shouldBeIncluded = I18n.ShouldIncludeLocale(questionedLocale, primaryLocale, priorityListLocale); + expect(shouldBeIncluded).toEqual(true); + }); + + it("Can determine if basic locales match", () => { + const want = "en-US"; + const have = "en-US"; + expect(I18n.DoLocalesMatch(want, have)).toEqual(true); + }); + + it("Can determine if wildcard locales match", () => { + const want = "en-*"; + const have = "en-US"; + expect(I18n.DoLocalesMatch(want, have)).toEqual(true); + }); + + it("Can determine if locales do not match", () => { + const want = "en-US"; + const have = "ja-JP"; + expect(I18n.DoLocalesMatch(want, have)).toEqual(false); + }); + }); + + describe("Crafts strings object correctly", () => { + it("Properly adds locale key", () => { + i18n.addStrings({ + example: { + stringKey: "String Value" + } + }, "en-US"); + + expect(i18n.strings.example.stringKey).toEqual({ + "en-US": "String Value" + }); + }); + + it("Handles deep objects", () => { + i18n.addStrings({ + example: { + deepExampleKey: { + stringKey: "String Value" + } + } + }, "en-US"); + + expect(i18n.strings.example.deepExampleKey.stringKey).toEqual({ + "en-US": "String Value" + }); + }); + + it("Adds new locales to existing objects", () => { + i18n.addStrings({ + example: { + stringKey: "Hello Pulsar" + } + }, "en-US"); + i18n.addStrings({ + example: { + stringKey: "Hola Pulsar" + } + }, "es-MX"); + + expect(i18n.strings.example.stringKey).toEqual({ + "en-US": "Hello Pulsar", + "es-MX": "Hola Pulsar" + }); + }); + + it("Adds new locale to object", () => { + i18n.addStrings({ + example: { + stringKey: "Hello Pulsar" + } + }, "en-US"); + + i18n.addString("example.stringKey", "Hola Pulsar", "es-MX"); + + expect(i18n.strings.example.stringKey).toEqual({ + "en-US": "Hello Pulsar", + "es-MX": "Hola Pulsar" + }); + }); + }); + + describe("Is able to translate properly", () => { + + beforeEach(() => { + const primary = "es-MX"; + const priorityListLocale = [ "zh-Hant" ]; + i18n.localeFallbackList = I18n.LocaleNegotiation(primary, priorityListLocale); + }); + + it("Returns the proper string based on user setting", () => { + i18n.addStrings({ + example: { + stringKey: "Hello Pulsar" + } + }, "en-US"); + + i18n.addStrings({ + example: { + stringKey: "Hola Pulsar" + } + }, "es-MX"); + + expect(i18n.t("example.stringKey")).toEqual("Hola Pulsar"); + }); + + it("Handles ICU MessageFormat Replacements", () => { + i18n.addStrings({ + example: { + stringKey: "Hello {value}" + } + }, "en-US"); + + expect(i18n.t("example.stringKey", { value: "confused-Techie" })).toEqual( + "Hello confused-Techie" + ); + }); + + it("Handles namespace translations", () => { + i18n.addStrings({ + example: { + stringKey: "Hello Pulsar" + } + }, "en-US"); + + const t = i18n.getT("example"); + + expect(t.t("stringKey")).toEqual("Hello Pulsar"); + }); + }); + + describe("Translates LocaleLabels", () => { + it("Identifies a LocaleLabel", () => { + const str1 = "%this-is-a-locale-label%"; + const str2 = "this-is-not-a-locale-label"; + const str3 = "%nor-is-this"; + const str4 = "%or-%this"; + + expect(i18n.isAutoTranslateLabel(str1)).toEqual(true); + expect(i18n.isAutoTranslateLabel(str2)).toEqual(false); + expect(i18n.isAutoTranslateLabel(str3)).toEqual(false); + expect(i18n.isAutoTranslateLabel(str4)).toEqual(false); + }); + + it("Successfully translates a LocaleLabel", () => { + const primary = "es-MX"; + const priorityListLocale = [ "zh-Hant" ]; + i18n.localeFallbackList = I18n.LocaleNegotiation(primary, priorityListLocale); + + i18n.addStrings({ + example: { + stringKey: "Hello Pulsar" + } + }, "en-US"); + + const localeLabel = "%example.stringKey%"; + + expect(i18n.translateLabel(localeLabel)).toEqual("Hello Pulsar"); + }); + + it("Fallsback to the original label if unable to translate", () => { + const primary = "es-MX"; + const priorityListLocale = [ "zh-Hant" ]; + i18n.localeFallbackList = I18n.LocaleNegotiation(primary, priorityListLocale); + + const localeLabel = "%example.stringKey%"; + + expect(i18n.translateLabel(localeLabel)).toEqual("%example.stringKey%"); + }); + }); +}); From 977f1bda94b4509381e8c75ca80ff91e5a5d69d0 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Fri, 9 Aug 2024 18:27:09 -0700 Subject: [PATCH 09/12] Add LocaleLabel translation tests to context-menu and manus --- spec/context-menu-manager-spec.js | 53 +++++++++++++++++++++++++++++++ spec/menu-manager-spec.js | 27 ++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/spec/context-menu-manager-spec.js b/spec/context-menu-manager-spec.js index 2b2e92b657..5fcce5d811 100644 --- a/spec/context-menu-manager-spec.js +++ b/spec/context-menu-manager-spec.js @@ -518,5 +518,58 @@ describe('ContextMenuManager', function() { } ]); }); + + it('translates labels when a LocaleLabel is present', function() { + const I18n = require("../src/i18n.js"); + atom.i18n.localeFallbackList = I18n.LocaleNegotiation( + "es-MX", + [ "zh-Hant" ] + ); + atom.i18n.addStrings({ + example: { + stringKey: "Hello Pulsar", + otherStringKey: "Goodbye Pulsar" + } + }, "en-US"); + + contextMenu.add({ + '.parent': [ + { + label: '%example.stringKey%', + submenu: [ + { + label: 'My Command', + command: 'test:my-command', + after: ['test:my-other-command'] + }, + { + label: '%example.otherStringKey%', + command: 'test:my-other-command' + } + ] + } + ] + }); + const dispatchedEvent = { target: parent }; + expect(contextMenu.templateForEvent(dispatchedEvent)).toEqual([ + { + label: 'Hello Pulsar', + id: `Parent`, + submenu: [ + { + label: 'My Other Command', + id: 'My Other Command', + command: 'test:my-other-command' + }, + { + label: 'Goodbye Pulsar', + id: 'My Command', + command: 'test:my-command', + after: ['test:my-other-command'] + } + ] + } + ]); + }); }); }); diff --git a/spec/menu-manager-spec.js b/spec/menu-manager-spec.js index ea9cb5af70..a4322d057d 100644 --- a/spec/menu-manager-spec.js +++ b/spec/menu-manager-spec.js @@ -104,6 +104,33 @@ describe('MenuManager', function() { submenu: [{ label: 'B', id: 'B', command: 'b' }] }); }); + + it('translates LocaleLabels', function() { + const I18n = require("../src/i18n.js"); + atom.i18n.localeFallbackList = I18n.LocaleNegotiation( + "es-MX", + [ "zh-Hant" ] + ); + atom.i18n.addStrings({ + example: { + stringKey: "Hello Pulsar", + otherStringKey: "Goodbye Pulsar" + } + }, "en-US"); + + const disposable = menu.add([ + { label: '%example.stringKey%', submenu: [{ label: '%example.otherStringKey%', command: 'b' }] } + ]); + expect(menu.template).toEqual([ + { + label: 'Hello Pulsar', + id: 'A', + submenu: [{ label: 'Goodbye Pulsar', id: 'B', command: 'b' }] + } + ]); + disposable.dispose(); + expect(menu.template).toEqual([]); + }); }); describe('::update()', function() { From 080708419ea41c6463e4dc4cd2db47a9728d3a70 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Fri, 9 Aug 2024 18:37:19 -0700 Subject: [PATCH 10/12] Fix errors in tests --- spec/i18n-spec.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/spec/i18n-spec.js b/spec/i18n-spec.js index f87dcf3439..1e09e3f2af 100644 --- a/spec/i18n-spec.js +++ b/spec/i18n-spec.js @@ -18,9 +18,9 @@ describe("I18n", () => { "zh-Hant-CN-x-private1-private2", "en-US" ]; - const fallbackArray = I18n.LocaleNegotiation(primaryLocale, priorityListLocale); + const fallbackSet = I18n.LocaleNegotiation(primaryLocale, priorityListLocale); - expect(fallbackArray).toEqual([ + expect(Array.from(fallbackSet)).toEqual([ "es-MX", "es", "zh-Hant-CN-x-private1-private2", @@ -36,9 +36,9 @@ describe("I18n", () => { it("Locale Lookup Filtering Fallback Pattern Array: Excludes duplicates from hardcoded fallback", () => { const primaryLocale = "en-US"; const priorityListLocale = [ "es-MX" ]; - const fallbackArray = I18n.LocaleNegotiation(primaryLocale, priorityListLocale); + const fallbackSet = I18n.LocaleNegotiation(primaryLocale, priorityListLocale); - expect(fallbackArray).toEqual([ + expect(Array.from(fallbackSet)).toEqual([ "en-US", "en", "es-MX", @@ -163,7 +163,7 @@ describe("I18n", () => { example: { stringKey: "Hello Pulsar" } - }, "en-US"); + }, "en"); i18n.addStrings({ example: { @@ -179,7 +179,7 @@ describe("I18n", () => { example: { stringKey: "Hello {value}" } - }, "en-US"); + }, "en"); expect(i18n.t("example.stringKey", { value: "confused-Techie" })).toEqual( "Hello confused-Techie" @@ -191,7 +191,7 @@ describe("I18n", () => { example: { stringKey: "Hello Pulsar" } - }, "en-US"); + }, "en"); const t = i18n.getT("example"); @@ -221,14 +221,14 @@ describe("I18n", () => { example: { stringKey: "Hello Pulsar" } - }, "en-US"); + }, "en"); const localeLabel = "%example.stringKey%"; expect(i18n.translateLabel(localeLabel)).toEqual("Hello Pulsar"); }); - it("Fallsback to the original label if unable to translate", () => { + it("Falls back to the original label if unable to translate", () => { const primary = "es-MX"; const priorityListLocale = [ "zh-Hant" ]; i18n.localeFallbackList = I18n.LocaleNegotiation(primary, priorityListLocale); From 63814195344a2088e89ec751cc84ff0c52e8669b Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Fri, 9 Aug 2024 19:11:19 -0700 Subject: [PATCH 11/12] Update `pulsar.en.cson` with new format --- locales/pulsar.en.cson | 24 +++++++++++------------- menus/win32.cson | 4 ++-- src/config-schema.js | 2 +- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/locales/pulsar.en.cson b/locales/pulsar.en.cson index f187576377..9fe490ac32 100644 --- a/locales/pulsar.en.cson +++ b/locales/pulsar.en.cson @@ -1,15 +1,13 @@ -'core': { - 'welcomeString': 'Hello Pulsar!' - 'goodbyeString': 'Goodbye Pulsar!' -} -'config': { - 'excludeVcsIgnoredPaths': { - 'title': 'Exclude VCS Ignored Paths' +'pulsar': { + 'config': { + 'excludeVcsIgnoredPaths': { + 'title': 'Exclude VCS Ignored Paths' + } + } + 'menu': { + 'settings-view:view-installed-packages': 'Open Package Manager' + } + 'context-menu': { + 'core:undo': 'Undo' } -} -'menu': { - 'settings-view:view-installed-packages': 'Open Package Manager' -} -'context-menu': { - 'core:undo': 'Undo' } diff --git a/menus/win32.cson b/menus/win32.cson index ffd1c763d7..9e87ac37dc 100644 --- a/menus/win32.cson +++ b/menus/win32.cson @@ -178,7 +178,7 @@ { label: '&Packages' submenu: [ - { label: '%menu.settings-view:view-installed-packages%', command: 'settings-view:view-installed-packages' } + { label: '%pulsar.menu.settings-view:view-installed-packages%', command: 'settings-view:view-installed-packages' } { type: 'separator' } ] } @@ -209,7 +209,7 @@ 'context-menu': 'atom-text-editor, .overlayer': [ - {label: '%context-menu.core:undo%', command: 'core:undo'} + {label: '%pulsar.context-menu.core:undo%', command: 'core:undo'} {label: 'Redo', command: 'core:redo'} {type: 'separator'} {label: 'Cut', command: 'core:cut'} diff --git a/src/config-schema.js b/src/config-schema.js index 7475700a6e..e346e81f5b 100644 --- a/src/config-schema.js +++ b/src/config-schema.js @@ -25,7 +25,7 @@ const configSchema = { excludeVcsIgnoredPaths: { type: 'boolean', default: true, - title: '%config.excludeVcsIgnoredPaths.title%', + title: '%pulsar.config.excludeVcsIgnoredPaths.title%', description: "Files and directories ignored by the current project's VCS will be ignored by some packages, such as the fuzzy finder and find and replace. For example, projects using Git have these paths defined in the .gitignore file. Individual packages might have additional config settings for ignoring VCS ignored files and folders." }, From 0783bb8fbe98526eb8aa3a6d525db8fb71b8eeb2 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Fri, 9 Aug 2024 19:57:35 -0700 Subject: [PATCH 12/12] Properly set fallback locale --- spec/context-menu-manager-spec.js | 2 +- spec/menu-manager-spec.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/context-menu-manager-spec.js b/spec/context-menu-manager-spec.js index 5fcce5d811..140b6dd4e8 100644 --- a/spec/context-menu-manager-spec.js +++ b/spec/context-menu-manager-spec.js @@ -530,7 +530,7 @@ describe('ContextMenuManager', function() { stringKey: "Hello Pulsar", otherStringKey: "Goodbye Pulsar" } - }, "en-US"); + }, "en"); contextMenu.add({ '.parent': [ diff --git a/spec/menu-manager-spec.js b/spec/menu-manager-spec.js index a4322d057d..bb4afd5999 100644 --- a/spec/menu-manager-spec.js +++ b/spec/menu-manager-spec.js @@ -116,7 +116,7 @@ describe('MenuManager', function() { stringKey: "Hello Pulsar", otherStringKey: "Goodbye Pulsar" } - }, "en-US"); + }, "en"); const disposable = menu.add([ { label: '%example.stringKey%', submenu: [{ label: '%example.otherStringKey%', command: 'b' }] }