From 66bb07295f6e078d842aac5ac1b479dffb95047d Mon Sep 17 00:00:00 2001 From: Meadowsys Date: Fri, 8 Sep 2023 19:00:40 -0700 Subject: [PATCH 01/17] smol amount of like, setup i suppose taking it slooooowwwwww and making sure not to break the stupid thing again --- package.json | 2 ++ src/atom-environment.js | 7 ++++++ src/i18n.js | 6 +++++ yarn.lock | 55 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 src/i18n.js diff --git a/package.json b/package.json index a85921a742..29f4823da5 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "dependencies": { "@atom/source-map-support": "^0.3.4", "@babel/core": "7.18.6", + "@formatjs/icu-messageformat-parser": "^2.6.1", "about": "file:packages/about", "archive-view": "file:packages/archive-view", "async": "3.2.4", @@ -83,6 +84,7 @@ "grim": "2.0.3", "image-view": "file:packages/image-view", "incompatible-packages": "file:packages/incompatible-packages", + "intl-messageformat": "^10.5.1", "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 13682f3e76..b25fae8937 100644 --- a/src/atom-environment.js +++ b/src/atom-environment.js @@ -45,6 +45,7 @@ const TextEditor = require('./text-editor'); const TextBuffer = require('text-buffer'); const TextEditorRegistry = require('./text-editor-registry'); const StartupTime = require('./startup-time'); +const I18n = require("./i18n"); const { getReleaseChannel } = require('./get-app-details.js'); const packagejson = require("../package.json"); @@ -138,6 +139,12 @@ class AtomEnvironment { uriHandlerRegistry: this.uriHandlerRegistry }); + /** @type {I18n} */ + this.i18n = new I18n({ + config: this.config, + packages: this.packages + }); + /** @type {ThemeManager} */ this.themes = new ThemeManager({ packageManager: this.packages, diff --git a/src/i18n.js b/src/i18n.js new file mode 100644 index 0000000000..5196ac8550 --- /dev/null +++ b/src/i18n.js @@ -0,0 +1,6 @@ +module.exports = class I18n { + constructor({ config, packages }) { + this.config = config; + this.packages = packages; + } +} diff --git a/yarn.lock b/yarn.lock index 845064f393..9368b62924 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1431,6 +1431,45 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" +"@formatjs/ecma402-abstract@1.17.1": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.17.1.tgz#6ac7d6a1d1c9c8eff76ab6ed949f2a5cbe424030" + integrity sha512-N2sjSUrmsEoynG8Q61pkrKlJ9PxcUGxJke1x3301aGyprGgl58wHWhgGUnzTfS4OHNNNQDxzjcXVp1t5fGW6yQ== + dependencies: + "@formatjs/intl-localematcher" "0.4.1" + 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.6.1", "@formatjs/icu-messageformat-parser@^2.6.1": + version "2.6.1" + resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.6.1.tgz#ca497d5a2bff641dc0978bd9b64d1d02597980cb" + integrity sha512-dTDNupwdovxT1xDXC96zzPUua/XrxTQTOulJZSvaJP0pt3rr/cGR/tq4d7BnxY9oqPZpc4fjWBmrRlhcUyBSiw== + dependencies: + "@formatjs/ecma402-abstract" "1.17.1" + "@formatjs/icu-skeleton-parser" "1.6.1" + tslib "^2.4.0" + +"@formatjs/icu-skeleton-parser@1.6.1": + version "1.6.1" + resolved "https://registry.yarnpkg.com/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.6.1.tgz#3647f41b82e362c08bb80bd9b653c7eb6ff31118" + integrity sha512-/LQ6ovxYd8FQjVLmbV+WmuFy86o+JTc0cIQuWixuLuUMfRRif8eUQw3vPK5hx7C/g1UVmKAaOcYRTEsvyEGz9g== + dependencies: + "@formatjs/ecma402-abstract" "1.17.1" + tslib "^2.4.0" + +"@formatjs/intl-localematcher@0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.4.1.tgz#af63e2c065731a33f6fed36dc85058009a7f8062" + integrity sha512-Fs4MhhHlLC0RrspX2u2KP7zlwL9eHrBZsOBxaPOeqrCZYLaOUK4cYXQ1ErpAB0HnGV/GUXNa5smzV/7jCuRzxg== + 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" @@ -1895,7 +1934,6 @@ abbrev@1: version "1.9.1" dependencies: etch "^0.14.1" - semver "^7.3.8" acorn-jsx@^5.3.2: version "5.3.2" @@ -5484,6 +5522,16 @@ interpret@^1.0.0: resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== +intl-messageformat@^10.5.1: + version "10.5.1" + resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-10.5.1.tgz#40304cbde01c8cb2236e11ac8c827642bed474d0" + integrity sha512-irEmjxHq0f1MHviQr3Q4ToF9EgYbnXDq2/R9MRTTveGKHgy6VZ29hQxswu4trqWaX7T6njKxSoKVG92OSz0U5Q== + dependencies: + "@formatjs/ecma402-abstract" "1.17.1" + "@formatjs/fast-memoize" "2.2.0" + "@formatjs/icu-messageformat-parser" "2.6.1" + 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" @@ -9497,6 +9545,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.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" From 078d29861b7eab66531a05e532d47fbf272b6c31 Mon Sep 17 00:00:00 2001 From: Meadowsys Date: Mon, 11 Sep 2023 23:27:27 -0700 Subject: [PATCH 02/17] nevermind we rip it out and rethink it from the ground up this time i'm going to use smaller classes as building blocks and "compose" them. Language` (internal and might be renamed) handles _one_ language for _one_ package --- src/atom-environment.js | 16 ++-- src/i18n.js | 176 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 182 insertions(+), 10 deletions(-) diff --git a/src/atom-environment.js b/src/atom-environment.js index b25fae8937..3b88b903a3 100644 --- a/src/atom-environment.js +++ b/src/atom-environment.js @@ -45,7 +45,7 @@ const TextEditor = require('./text-editor'); const TextBuffer = require('text-buffer'); const TextEditorRegistry = require('./text-editor-registry'); const StartupTime = require('./startup-time'); -const I18n = require("./i18n"); +// const I18n = require("./i18n"); const { getReleaseChannel } = require('./get-app-details.js'); const packagejson = require("../package.json"); @@ -139,11 +139,11 @@ class AtomEnvironment { uriHandlerRegistry: this.uriHandlerRegistry }); - /** @type {I18n} */ - this.i18n = new I18n({ - config: this.config, - packages: this.packages - }); + // /** @type {I18n} */ + // this.i18n = new I18n({ + // config: this.config, + // packages: this.packages + // }); /** @type {ThemeManager} */ this.themes = new ThemeManager({ @@ -289,6 +289,10 @@ class AtomEnvironment { this.commands.attach(this.window); + // this.i18n.initialise({ + // resourcePath + // }); + this.styles.initialize({ configDirPath: this.configDirPath }); this.packages.initialize({ devMode, diff --git a/src/i18n.js b/src/i18n.js index 5196ac8550..08ca6b2e7e 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -1,6 +1,174 @@ -module.exports = class I18n { - constructor({ config, packages }) { - this.config = config; - this.packages = packages; +const { default: IntlMessageFormat } = require("intl-messageformat"); +const { parse: parseString } = require("@formatjs/icu-messageformat-parser"); + +class Language { + constructor({ langStrings, locale, cachedASTs }) { + /** @type {LanguageStrings} */ + this.langStrings = langStrings || {}; + /** @type {string} */ + this.locale = locale; + /** @type {LanguageASTCache} */ + this.cachedASTs = cachedASTs || {}; + /** @type {LanguageFormatterCache} */ + this.cachedFormatters = {}; + } + + /** + * @param {string} keystr + * @param {{ [k: string]: string }} opts + * @return {string} + */ + t(keystr, opts = {}) { + const key = keystr.split("."); + guardPrototypePollution(key); + + const formatter = this._getFormatterMaybe(key); + if (formatter) { + const formatted = /** @type {string} */ (formatter.format(opts)); + return formatted; + } + return keystr; + } + + /** + * @param {Array} key + * @return {IntlMessageFormat | undefined} + */ + _getFormatterMaybe(key) { + let value = this.cachedFormatters; + + // iterate through all parts of the key except the last + // we shall check that manually + const lastKeyPos = key.length - 1; + for (let i = 0; i < lastKeyPos; i++) { + // value should always be LanguageFormatterCache at the start of this block + const k = key[i]; + const v = value[k]; + + if (!v) { + /** @type {LanguageFormatterCache} */ + const cache = {}; + value[k] = cache; + value = cache; + continue; + } + + if (v instanceof IntlMessageFormat) { + return; + } + + value = v; + } + + const last = key[lastKeyPos]; + + if (!value[last]) { + const ast = this._getASTMaybe(key); + if (!ast) return; + + const formatter = new IntlMessageFormat(ast.ast, this.locale); + value[last] = formatter; + return formatter; + } + + const v = value[last]; + if (v instanceof IntlMessageFormat) { + return v; + } + + return; + } + + /** + * @param {Array} key + * @return {AST | undefined} + */ + _getASTMaybe(key) { + let value = this.cachedASTs; + + const lastKeyPos = key.length - 1; + for (let i = 0; i < lastKeyPos; i++) { + const k = key[i]; + const v = value.keys[k]; + + if (!v) { + /** @type {LanguageASTCache} */ + const cache = { keys: {} }; + value.keys[k] = cache; + value = cache; + continue; + } + + if (isAST(v)) { + return; + } + + value = v; + } + + const last = key[lastKeyPos]; + + if (!value.keys[last]) { + const string = this._getStringMaybe(key); + if (!string) return; + + const ast = parseString(string); + return { ast }; + } + + const v = value.keys[last] + if (isAST(v)) { + return v; + } + + return; + } + + /** + * @param {Array} key + * @return {string | undefined} + */ + _getStringMaybe(key) { + let value = this.langStrings; + + const lastKeyPos = key.length - 1; + for (let i = 0; i < lastKeyPos; i++) { + const k = key[i]; + const v = value[k]; + if (!v || typeof v === "string") { + return; + } + value = v; + } + + const v = value[lastKeyPos]; + if (typeof v === "string") return v; + } +} + +/** + * @param {CacheOrAST} obj + * @return {obj is AST} + */ +function isAST(obj) { + return "ast" in obj; +} + +/** + * @param {Array} key + */ +function guardPrototypePollution(key) { + if (key.indexOf("__proto__") >= 0) { + throw new Error(`prototype pollution in key "${key.join(".")}" was detected and prevented`); } } + +/** + * @typedef {import("@formatjs/icu-messageformat-parser").MessageFormatElement} MessageFormatElement + * @typedef {{ ast: Array }} AST + * @typedef {{ keys: { [x: string]: CacheOrAST } }} LanguageASTCache + * @typedef {AST | LanguageASTCache} CacheOrAST + * + * @typedef {{ [x: string]: LanguageStrings | string }} LanguageStrings + * @typedef {{ [x: string]: LanguageFormatterCache | IntlMessageFormat }} LanguageFormatterCache + */ From edb181bb4fc3dba6a807f81b061cb1cb1e4af27d Mon Sep 17 00:00:00 2001 From: Meadowsys Date: Mon, 11 Sep 2023 23:48:53 -0700 Subject: [PATCH 03/17] this shouldn't have this --- src/i18n.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n.js b/src/i18n.js index 08ca6b2e7e..fedf732793 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -4,7 +4,7 @@ const { parse: parseString } = require("@formatjs/icu-messageformat-parser"); class Language { constructor({ langStrings, locale, cachedASTs }) { /** @type {LanguageStrings} */ - this.langStrings = langStrings || {}; + this.langStrings = langStrings; /** @type {string} */ this.locale = locale; /** @type {LanguageASTCache} */ From f1ce88094e4e660473007576e800bedc1de1651b Mon Sep 17 00:00:00 2001 From: Meadowsys Date: Tue, 12 Sep 2023 00:16:22 -0700 Subject: [PATCH 04/17] fix a few bugs and stuff --- src/i18n.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/i18n.js b/src/i18n.js index fedf732793..7dc21d7882 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -8,7 +8,7 @@ class Language { /** @type {string} */ this.locale = locale; /** @type {LanguageASTCache} */ - this.cachedASTs = cachedASTs || {}; + this.cachedASTs = cachedASTs || { keys: {} }; /** @type {LanguageFormatterCache} */ this.cachedFormatters = {}; } @@ -75,8 +75,6 @@ class Language { if (v instanceof IntlMessageFormat) { return v; } - - return; } /** @@ -112,16 +110,19 @@ class Language { const string = this._getStringMaybe(key); if (!string) return; - const ast = parseString(string); - return { ast }; + /** @type {AST} */ + const ast = { + ast: parseString(string) + }; + + value.keys[last] = ast; + return ast; } - const v = value.keys[last] + const v = value.keys[last]; if (isAST(v)) { return v; } - - return; } /** @@ -141,7 +142,8 @@ class Language { value = v; } - const v = value[lastKeyPos]; + const last = key[lastKeyPos] + const v = value[last]; if (typeof v === "string") return v; } } From b372e64129c477f45e52be0331dbeee82a52a754 Mon Sep 17 00:00:00 2001 From: Meadowsys Date: Tue, 12 Sep 2023 17:48:08 -0700 Subject: [PATCH 05/17] implement thing that manages one language for all packages --- src/i18n.js | 84 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 75 insertions(+), 9 deletions(-) diff --git a/src/i18n.js b/src/i18n.js index 7dc21d7882..b47f0b996e 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -2,12 +2,79 @@ const { default: IntlMessageFormat } = require("intl-messageformat"); const { parse: parseString } = require("@formatjs/icu-messageformat-parser"); class Language { - constructor({ langStrings, locale, cachedASTs }) { - /** @type {LanguageStrings} */ - this.langStrings = langStrings; - /** @type {string} */ + /** + * @param {object} opts + * @param {string} opts.locale + * @param {{ [k: string]: LanguageStrings }} opts.strings + * @param {{ [k: string]: LanguageASTCache }} [opts.cachedASTs] + */ + constructor({ locale, strings, cachedASTs }) { + this.strings = strings; + this.locale = locale; + this.cachedASTs = cachedASTs || {}; + /** @type {{ [k: string]: SinglePackageLanguage }} */ + this.packageLanguages = {}; + } + + /** + * @param {string} keystr + * @param {{ [k: string]: string }} opts + * @return {string | undefined} + */ + tMaybe(keystr, opts = {}) { + const nsIndex = keystr.indexOf("."); + if (nsIndex < 0) return; + + const pkgName = keystr.substring(0, nsIndex); + const keystrWithoutPkgName = keystr.substring(nsIndex + 1); + + const pkg = this._getPackageObj(pkgName); + return pkg?.tMaybe(keystrWithoutPkgName, opts); + } + + /** + * @param {string} pkgName + */ + _getPackageObj(pkgName) { + if (this.packageLanguages[pkgName]) return this.packageLanguages[pkgName]; + if (!this.strings[pkgName]) return; + + this.cachedASTs[pkgName] ??= { keys: {} }; + + this.packageLanguages[pkgName] = new SinglePackageLanguage({ + locale: this.locale, + strings: this.strings[pkgName], + cachedASTs: this.cachedASTs[pkgName] + }); + + return this.packageLanguages[pkgName]; + } + + /** + * @param {object} opts + * @param {string} opts.pkgName + * @param {LanguageStrings} opts.strings + * @param {LanguageASTCache} [opts.cachedASTs] + */ + addOrReplaceStringsForPackage({ pkgName, strings, cachedASTs }) { + this.strings[pkgName] = strings; + this.cachedASTs[pkgName] = cachedASTs || { keys: {} }; + delete this.packageLanguages[pkgName]; + // side effect: `this.packageLanguages[pkgName]` gets remade + this._getPackageObj(pkgName); + } +} + +class SinglePackageLanguage { + /** + * @param {object} opts + * @param {string} opts.locale + * @param {LanguageStrings} opts.strings + * @param {LanguageASTCache} [opts.cachedASTs] + */ + constructor({ locale, strings, cachedASTs }) { + this.strings = strings; this.locale = locale; - /** @type {LanguageASTCache} */ this.cachedASTs = cachedASTs || { keys: {} }; /** @type {LanguageFormatterCache} */ this.cachedFormatters = {}; @@ -16,9 +83,9 @@ class Language { /** * @param {string} keystr * @param {{ [k: string]: string }} opts - * @return {string} + * @return {string | undefined} */ - t(keystr, opts = {}) { + tMaybe(keystr, opts = {}) { const key = keystr.split("."); guardPrototypePollution(key); @@ -27,7 +94,6 @@ class Language { const formatted = /** @type {string} */ (formatter.format(opts)); return formatted; } - return keystr; } /** @@ -130,7 +196,7 @@ class Language { * @return {string | undefined} */ _getStringMaybe(key) { - let value = this.langStrings; + let value = this.strings; const lastKeyPos = key.length - 1; for (let i = 0; i < lastKeyPos; i++) { From 9058ae3bb22290ae103a86c47e606d7c7762e9ad Mon Sep 17 00:00:00 2001 From: Meadowsys Date: Tue, 12 Sep 2023 20:17:15 -0700 Subject: [PATCH 06/17] well... decided to start over, committing now to have like a snapshot --- src/i18n.js | 255 ++++++++-------------------------------------------- 1 file changed, 36 insertions(+), 219 deletions(-) diff --git a/src/i18n.js b/src/i18n.js index b47f0b996e..c5769f4420 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -1,242 +1,59 @@ const { default: IntlMessageFormat } = require("intl-messageformat"); const { parse: parseString } = require("@formatjs/icu-messageformat-parser"); -class Language { - /** - * @param {object} opts - * @param {string} opts.locale - * @param {{ [k: string]: LanguageStrings }} opts.strings - * @param {{ [k: string]: LanguageASTCache }} [opts.cachedASTs] - */ - constructor({ locale, strings, cachedASTs }) { - this.strings = strings; - this.locale = locale; - this.cachedASTs = cachedASTs || {}; - /** @type {{ [k: string]: SinglePackageLanguage }} */ - this.packageLanguages = {}; - } - - /** - * @param {string} keystr - * @param {{ [k: string]: string }} opts - * @return {string | undefined} - */ - tMaybe(keystr, opts = {}) { - const nsIndex = keystr.indexOf("."); - if (nsIndex < 0) return; - - const pkgName = keystr.substring(0, nsIndex); - const keystrWithoutPkgName = keystr.substring(nsIndex + 1); - const pkg = this._getPackageObj(pkgName); - return pkg?.tMaybe(keystrWithoutPkgName, opts); - } - - /** - * @param {string} pkgName - */ - _getPackageObj(pkgName) { - if (this.packageLanguages[pkgName]) return this.packageLanguages[pkgName]; - if (!this.strings[pkgName]) return; - - this.cachedASTs[pkgName] ??= { keys: {} }; - - this.packageLanguages[pkgName] = new SinglePackageLanguage({ - locale: this.locale, - strings: this.strings[pkgName], - cachedASTs: this.cachedASTs[pkgName] - }); - - return this.packageLanguages[pkgName]; - } +class Localisations {} +/** + * manages strings of all languages for a single package + * (in other words, manages `PackageLocalisations` instances) + */ +class PackageLocalisations { /** * @param {object} opts * @param {string} opts.pkgName - * @param {LanguageStrings} opts.strings - * @param {LanguageASTCache} [opts.cachedASTs] + * @param {PackageStrings} opts.strings + * @param {PackageASTCache} [opts.asts] */ - addOrReplaceStringsForPackage({ pkgName, strings, cachedASTs }) { - this.strings[pkgName] = strings; - this.cachedASTs[pkgName] = cachedASTs || { keys: {} }; - delete this.packageLanguages[pkgName]; - // side effect: `this.packageLanguages[pkgName]` gets remade - this._getPackageObj(pkgName); - } + constructor({}) {} } -class SinglePackageLanguage { +/** + * manages strings for a single locale of a single package + */ +// TODO someone please help me find a better name ~meadowsys +class SingleLanguageLocalisations { /** * @param {object} opts * @param {string} opts.locale - * @param {LanguageStrings} opts.strings - * @param {LanguageASTCache} [opts.cachedASTs] - */ - constructor({ locale, strings, cachedASTs }) { - this.strings = strings; - this.locale = locale; - this.cachedASTs = cachedASTs || { keys: {} }; - /** @type {LanguageFormatterCache} */ - this.cachedFormatters = {}; - } - - /** - * @param {string} keystr - * @param {{ [k: string]: string }} opts - * @return {string | undefined} - */ - tMaybe(keystr, opts = {}) { - const key = keystr.split("."); - guardPrototypePollution(key); - - const formatter = this._getFormatterMaybe(key); - if (formatter) { - const formatted = /** @type {string} */ (formatter.format(opts)); - return formatted; - } - } - - /** - * @param {Array} key - * @return {IntlMessageFormat | undefined} - */ - _getFormatterMaybe(key) { - let value = this.cachedFormatters; - - // iterate through all parts of the key except the last - // we shall check that manually - const lastKeyPos = key.length - 1; - for (let i = 0; i < lastKeyPos; i++) { - // value should always be LanguageFormatterCache at the start of this block - const k = key[i]; - const v = value[k]; - - if (!v) { - /** @type {LanguageFormatterCache} */ - const cache = {}; - value[k] = cache; - value = cache; - continue; - } - - if (v instanceof IntlMessageFormat) { - return; - } - - value = v; - } - - const last = key[lastKeyPos]; - - if (!value[last]) { - const ast = this._getASTMaybe(key); - if (!ast) return; - - const formatter = new IntlMessageFormat(ast.ast, this.locale); - value[last] = formatter; - return formatter; - } - - const v = value[last]; - if (v instanceof IntlMessageFormat) { - return v; - } - } - - /** - * @param {Array} key - * @return {AST | undefined} - */ - _getASTMaybe(key) { - let value = this.cachedASTs; - - const lastKeyPos = key.length - 1; - for (let i = 0; i < lastKeyPos; i++) { - const k = key[i]; - const v = value.keys[k]; - - if (!v) { - /** @type {LanguageASTCache} */ - const cache = { keys: {} }; - value.keys[k] = cache; - value = cache; - continue; - } - - if (isAST(v)) { - return; - } - - value = v; - } - - const last = key[lastKeyPos]; - - if (!value.keys[last]) { - const string = this._getStringMaybe(key); - if (!string) return; - - /** @type {AST} */ - const ast = { - ast: parseString(string) - }; - - value.keys[last] = ast; - return ast; - } - - const v = value.keys[last]; - if (isAST(v)) { - return v; - } - } - - /** - * @param {Array} key - * @return {string | undefined} + * @param {Strings} opts.strings + * @param {ASTCache} [opts.asts] */ - _getStringMaybe(key) { - let value = this.strings; - - const lastKeyPos = key.length - 1; - for (let i = 0; i < lastKeyPos; i++) { - const k = key[i]; - const v = value[k]; - if (!v || typeof v === "string") { - return; - } - value = v; - } - - const last = key[lastKeyPos] - const v = value[last]; - if (typeof v === "string") return v; - } + constructor({}) {} } /** - * @param {CacheOrAST} obj - * @return {obj is AST} + * "basic" types + * @typedef {import("@formatjs/icu-messageformat-parser").MessageFormatElement} MessageFormatElement + * @typedef {{ ast: Array }} AST + * @typedef {{ items: { [k: string]: OneOrManyASTs } }} ManyASTs + * @typedef {AST | ManyASTs} OneOrManyASTs */ -function isAST(obj) { - return "ast" in obj; -} - /** - * @param {Array} key + * types for `Localisations` + * @typedef {{ [k: string]: PackageASTCache }} AllAstCache + * @typedef {{ [k: string]: PackageStrings }} AllStrings + * @typedef {{ [k: string]: PackageFormatterCache }} AllFormatterCache */ -function guardPrototypePollution(key) { - if (key.indexOf("__proto__") >= 0) { - throw new Error(`prototype pollution in key "${key.join(".")}" was detected and prevented`); - } -} - /** - * @typedef {import("@formatjs/icu-messageformat-parser").MessageFormatElement} MessageFormatElement - * @typedef {{ ast: Array }} AST - * @typedef {{ keys: { [x: string]: CacheOrAST } }} LanguageASTCache - * @typedef {AST | LanguageASTCache} CacheOrAST - * - * @typedef {{ [x: string]: LanguageStrings | string }} LanguageStrings - * @typedef {{ [x: string]: LanguageFormatterCache | IntlMessageFormat }} LanguageFormatterCache + * types for `PackageLocalisations` + * @typedef {{ [k: string]: ASTCache }} PackageASTCache + * @typedef {{ [k: string]: Strings }} PackageStrings + * @typedef {{ [k: string]: FormatterCache }} PackageFormatterCache + */ +/** + * used in `SingleLanguageLocalisations` + * @typedef {{ [k: string]: OneOrManyASTs }} ASTCache + * @typedef {{ [k: string]: string | Strings }} Strings + * @typedef {{ [k: string]: IntlMessageFormat | FormatterCache }} FormatterCache */ From e0079ed8bb611c7aeef3c71388c0a9815495fb96 Mon Sep 17 00:00:00 2001 From: Meadowsys Date: Wed, 13 Sep 2023 03:19:14 -0700 Subject: [PATCH 07/17] finish redoing the thing again --- src/i18n.js | 238 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 228 insertions(+), 10 deletions(-) diff --git a/src/i18n.js b/src/i18n.js index c5769f4420..2dc0fc445c 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -1,8 +1,47 @@ const { default: IntlMessageFormat } = require("intl-messageformat"); -const { parse: parseString } = require("@formatjs/icu-messageformat-parser"); +const { parse: parseToAST } = require("@formatjs/icu-messageformat-parser"); +class Localisations { + /** + * @param {object} opts + * @param {Array} opts.locales + */ + constructor({ locales }) { + this.locales = locales; + /** @type {{ [k: string]: PackageLocalisations }} */ + this.packages = {}; + } -class Localisations {} + /** + * @param {Key} _keystr + * @param {Opts} opts + */ + t(_keystr, opts = {}) { + const i = _keystr.indexOf("."); + if (i < 0) return fallback(_keystr, opts); + + const pkgName = _keystr.substring(0, i); + if (!this.packages[pkgName]) return fallback(_keystr, opts); + + const keystr = _keystr.substring(i + 1); + + return this.packages[pkgName].t(keystr, opts) ?? fallback(_keystr, opts); + } + + /** + * @param {object} opts + * @param {string} opts.pkgName + * @param {PackageStrings} opts.strings + * @param {PackageASTCache} [opts.asts] + */ + addPackage({ pkgName, strings, asts }) { + this.packages[pkgName] = new PackageLocalisations({ + locales: this.locales, + strings, + asts + }); + } +} /** * manages strings of all languages for a single package @@ -11,11 +50,36 @@ class Localisations {} class PackageLocalisations { /** * @param {object} opts - * @param {string} opts.pkgName + * @param {Array} opts.locales * @param {PackageStrings} opts.strings * @param {PackageASTCache} [opts.asts] */ - constructor({}) {} + constructor({ locales, strings: _strings, asts }) { + this.locales = locales; + /** @type {PackageASTCache} */ + this.asts = asts ?? {}; + /** @type {{ [k: string]: SingleLanguageLocalisations }} */ + this.localeObjs = {}; + + for (const [locale, strings] of Object.entries(_strings)) { + this.localeObjs[locale] = new SingleLanguageLocalisations({ + locale, + strings, + asts: (this.asts[locale] = this.asts[locale] ?? { items: {} }) + }); + } + } + + /** + * @param {Key} keystr + * @param {Opts} opts + */ + t(keystr, opts = {}) { + for (const locale of this.locales) { + const localised = this.localeObjs[locale]?.t(keystr, opts); + if (localised) return localised; + } + } } /** @@ -29,7 +93,159 @@ class SingleLanguageLocalisations { * @param {Strings} opts.strings * @param {ASTCache} [opts.asts] */ - constructor({}) {} + constructor({ locale, strings, asts }) { + this.locale = locale; + this.strings = strings; + /** @type {ASTCache} */ + this.asts = asts ?? { items: {} }; + /** @type {FormatterCache} */ + this.formatters = {}; + } + + /** + * @param {Key} keystr + * @param {Opts} opts + */ + t(keystr, opts = {}) { + const key = keystr.split("."); + guardPrototypePollution(key); + + const formatter = this._getFormatter(key); + if (formatter) { + const formatted = /** @type {string} */ (formatter.format(opts)); + return formatted; + } + } + + /** + * @param {SplitKey} key + */ + _getFormatter(key) { + let value = this.formatters; + + const lastKeyPos = key.length - 1; + for (let i = 0; i < lastKeyPos; i++) { + const k = key[i]; + const v = value[k]; + + if (!v) { + /** @type {FormatterCache} */ + const cache = {}; + value[k] = cache; + value = cache; + continue; + } + if (v instanceof IntlMessageFormat) return; + value = v; + } + + const k = key[lastKeyPos]; + const v = value[k]; + + if (!v) { + const ast = this._getAST(key); + if (!ast) return; + + const formatter = new IntlMessageFormat(ast.ast, this.locale); + value[k] = formatter; + return formatter; + } + + if (v instanceof IntlMessageFormat) return v; + } + + /** + * @param {SplitKey} key + */ + _getAST(key) { + let value = this.asts; + + const lastKeyPos = key.length - 1; + for (let i = 0; i < lastKeyPos; i++) { + const k = key[i]; + const v = value.items[k]; + + if (!v) { + /** @type {ManyASTs} */ + const cache = { items: {} }; + value[k] = cache; + value = cache; + continue; + } + if (isAST(v)) return; + value = v; + } + + const k = key[lastKeyPos]; + const v = value.items[k]; + + if (!v) { + const string = this._getString(key); + if (!string) return; + + /** @type {AST} */ + const ast = { + ast: parseToAST(string) + }; + + value.items[k] = ast; + return ast; + } + + if (isAST(v)) return v; + } + + /** + * @param {SplitKey} key + */ + _getString(key) { + let value = this.strings; + + const lastKeyPos = key.length - 1; + for (let i = 0; i < lastKeyPos; i++) { + const k = key[i]; + const v = value[k]; + + if (!v || typeof v === "string") return; + value = v; + } + + const k = key[lastKeyPos]; + const v = value[k]; + if (typeof v === "string") return v; + } +} + +/** + * @param {OneOrManyASTs} obj + * @return {obj is AST} + */ +function isAST(obj) { + return "ast" in obj; +} + +/** + * @param {SplitKey} key + */ +function guardPrototypePollution(key) { + if (key.includes("__proto__")) { + throw new Error(`prototype pollution in key "${key.join(".")}" was detected and prevented`); + } +} + +/** + * @param {Key} keystr + * @param {Opts} opts + */ +function fallback(keystr, opts) { + const optsArray = Object.entries(opts); + if (optsArray.length === 0) return keystr; + + return `${keystr}: { ${ + optsArray + .map(([k, v]) => `${JSON.stringify(k)}: ${JSON.stringify(v)}`) + .join(", ") + } }`; } /** @@ -38,22 +254,24 @@ class SingleLanguageLocalisations { * @typedef {{ ast: Array }} AST * @typedef {{ items: { [k: string]: OneOrManyASTs } }} ManyASTs * @typedef {AST | ManyASTs} OneOrManyASTs + * + * @typedef {string} Key + * @typedef {Array} SplitKey + * @typedef {{ [k: string]: string }} Opts */ /** * types for `Localisations` - * @typedef {{ [k: string]: PackageASTCache }} AllAstCache * @typedef {{ [k: string]: PackageStrings }} AllStrings - * @typedef {{ [k: string]: PackageFormatterCache }} AllFormatterCache + * @typedef {{ [k: string]: PackageASTCache }} AllAstCache */ /** * types for `PackageLocalisations` - * @typedef {{ [k: string]: ASTCache }} PackageASTCache * @typedef {{ [k: string]: Strings }} PackageStrings - * @typedef {{ [k: string]: FormatterCache }} PackageFormatterCache + * @typedef {{ [k: string]: ASTCache }} PackageASTCache */ /** * used in `SingleLanguageLocalisations` - * @typedef {{ [k: string]: OneOrManyASTs }} ASTCache * @typedef {{ [k: string]: string | Strings }} Strings + * @typedef {ManyASTs} ASTCache * @typedef {{ [k: string]: IntlMessageFormat | FormatterCache }} FormatterCache */ From 5bf8f860a040db8cb2be1934bdedaf5f8d0c8d94 Mon Sep 17 00:00:00 2001 From: Meadowsys Date: Wed, 13 Sep 2023 16:07:01 -0700 Subject: [PATCH 08/17] ?. --- src/i18n.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/i18n.js b/src/i18n.js index 2dc0fc445c..2ca791d8b3 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -21,11 +21,8 @@ class Localisations { if (i < 0) return fallback(_keystr, opts); const pkgName = _keystr.substring(0, i); - if (!this.packages[pkgName]) return fallback(_keystr, opts); - const keystr = _keystr.substring(i + 1); - - return this.packages[pkgName].t(keystr, opts) ?? fallback(_keystr, opts); + return this.packages[pkgName]?.t(keystr, opts) ?? fallback(_keystr, opts); } /** From 3e0ffb20b9274b97859d041ec54c005eaa0e0044 Mon Sep 17 00:00:00 2001 From: Meadowsys Date: Wed, 13 Sep 2023 16:57:29 -0700 Subject: [PATCH 09/17] implement a bit of actual i18n (config, not much else) --- src/atom-environment.js | 20 +++++++-------- src/i18n.js | 54 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 61 insertions(+), 13 deletions(-) diff --git a/src/atom-environment.js b/src/atom-environment.js index 3b88b903a3..c41b17a06c 100644 --- a/src/atom-environment.js +++ b/src/atom-environment.js @@ -45,7 +45,7 @@ const TextEditor = require('./text-editor'); const TextBuffer = require('text-buffer'); const TextEditorRegistry = require('./text-editor-registry'); const StartupTime = require('./startup-time'); -// const I18n = require("./i18n"); +const I18n = require("./i18n"); const { getReleaseChannel } = require('./get-app-details.js'); const packagejson = require("../package.json"); @@ -104,6 +104,10 @@ class AtomEnvironment { type: 'object', properties: _.clone(ConfigSchema) }); + /** @type {I18n} */ + this.i18n = new I18n({ + config: this.config + }); /** @type {KeymapManager} */ this.keymaps = new KeymapManager({ @@ -139,12 +143,6 @@ class AtomEnvironment { uriHandlerRegistry: this.uriHandlerRegistry }); - // /** @type {I18n} */ - // this.i18n = new I18n({ - // config: this.config, - // packages: this.packages - // }); - /** @type {ThemeManager} */ this.themes = new ThemeManager({ packageManager: this.packages, @@ -273,6 +271,10 @@ class AtomEnvironment { }); this.config.resetUserSettings(userSettings); + this.i18n.initialise({ + locales: ["en"] // TODO implement config + }); + if (projectSpecification != null && projectSpecification.config != null) { this.project.replace(projectSpecification); } @@ -289,10 +291,6 @@ class AtomEnvironment { this.commands.attach(this.window); - // this.i18n.initialise({ - // resourcePath - // }); - this.styles.initialize({ configDirPath: this.configDirPath }); this.packages.initialize({ devMode, diff --git a/src/i18n.js b/src/i18n.js index 2ca791d8b3..3721a12bc0 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -1,17 +1,67 @@ const { default: IntlMessageFormat } = require("intl-messageformat"); const { parse: parseToAST } = require("@formatjs/icu-messageformat-parser"); -class Localisations { +module.exports = class I18n { + /** + * @param {object} opts + * @param {import("./config")} opts.config + */ + constructor({ config }) { + this.config = config; + /** @type {Array} */ + this.locales = []; + this.localisations = new Localisations(); + + this.config.setSchema("core.languageSettings", { + type: "object", + description: "These settings currently require a full restart to take effect", + properties: { + primaryLanguage: { + type: "string", + order: 1, + default: "en", + // TODO get available languages for enum + }, + fallbackLanguages: { + type: "array", + order: 2, + description: "List of fallback languages in case something is not translated in your primary language. Note: `en` is always the last fallback language, to ensure that things at least show up.", + default: [], + items: { + type: "string" + // TODO consider array enum when enum array options are improved in settings UI? + } + } + } + }) + } + /** * @param {object} opts * @param {Array} opts.locales */ - constructor({ locales }) { + initialise({ locales }) { this.locales = locales; + this.localisations.initialise({ locales }); + } +} + +class Localisations { + constructor() { + /** @type {Array} */ + this.locales = []; /** @type {{ [k: string]: PackageLocalisations }} */ this.packages = {}; } + /** + * @param {object} opts + * @param {Array} opts.locales + */ + initialise({ locales }) { + this.locales = locales; + } + /** * @param {Key} _keystr * @param {Opts} opts From ae02b3db36027421e5af6eb71c4cafe01e911036 Mon Sep 17 00:00:00 2001 From: Meadowsys Date: Wed, 13 Sep 2023 20:25:00 -0700 Subject: [PATCH 10/17] WIP stuff?? --- src/atom-environment.js | 4 +-- src/i18n.js | 61 +++++++++++++++++++++++++++++++++++------ 2 files changed, 54 insertions(+), 11 deletions(-) diff --git a/src/atom-environment.js b/src/atom-environment.js index c41b17a06c..48a922131c 100644 --- a/src/atom-environment.js +++ b/src/atom-environment.js @@ -271,9 +271,7 @@ class AtomEnvironment { }); this.config.resetUserSettings(userSettings); - this.i18n.initialise({ - locales: ["en"] // TODO implement config - }); + this.i18n.initialise({ resourcePath }); if (projectSpecification != null && projectSpecification.config != null) { this.project.replace(projectSpecification); diff --git a/src/i18n.js b/src/i18n.js index 3721a12bc0..b5d7b4ce53 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -1,6 +1,10 @@ +const fs = require("fs-plus"); +const path = require("path"); const { default: IntlMessageFormat } = require("intl-messageformat"); const { parse: parseToAST } = require("@formatjs/icu-messageformat-parser"); +const supportedFileExts = ["cson", "json"]; + module.exports = class I18n { /** * @param {object} opts @@ -11,6 +15,7 @@ module.exports = class I18n { /** @type {Array} */ this.locales = []; this.localisations = new Localisations(); + this.resourcePath = ""; this.config.setSchema("core.languageSettings", { type: "object", @@ -38,11 +43,50 @@ module.exports = class I18n { /** * @param {object} opts - * @param {Array} opts.locales + * @param {string} opts.resourcePath */ - initialise({ locales }) { - this.locales = locales; - this.localisations.initialise({ locales }); + initialise({ resourcePath }) { + this.locales = [ + this.config.get("core.languageSettings.primaryLanguage"), + ...this.config.get("core.languageSettings.fallbackLanguages"), + "en" + ].map(l => l.toLowerCase()); + this.resourcePath = resourcePath; + + this.localisations.initialise({ locales: this.locales }); // TODO ast cache + + this.loadStringsForCore(); + } + + loadStringsForCore() {} + + loadStringsForPackage() {} + + /** + * @param {string} pkgName + * @param {string} i18nDirPath path to the i18n dir with the files in it + */ + _loadStringsAt(pkgName, i18nDirPath) { + /** @type {Array} */ + const filesArray = fs.readdirSync(i18nDirPath); + const files = new Set(filesArray.map(f => f.toLowerCase())); + + /** @type {PackageStrings} */ + const packageStrings = {}; + this.locales.forEach(locale => { + const ext = supportedFileExts.find(ext => files.has(`${locale}.${ext}`)); + if (!ext) return; + + const filename = `${locale}.${ext}`; + const filepath = path.join(i18nDirPath, filename); + + const strings = fs.readFileSync(filepath, "utf8"); + packageStrings[locale] = strings; + }); + this.localisations.addPackage({ + pkgName, + strings: packageStrings + }); } } @@ -57,9 +101,11 @@ class Localisations { /** * @param {object} opts * @param {Array} opts.locales + * @param {AllAstCache} [opts.asts] */ - initialise({ locales }) { + initialise({ locales, asts }) { this.locales = locales; + this.asts = asts; } /** @@ -79,13 +125,12 @@ class Localisations { * @param {object} opts * @param {string} opts.pkgName * @param {PackageStrings} opts.strings - * @param {PackageASTCache} [opts.asts] */ - addPackage({ pkgName, strings, asts }) { + addPackage({ pkgName, strings }) { this.packages[pkgName] = new PackageLocalisations({ locales: this.locales, strings, - asts + asts: this.asts?.[pkgName] }); } } From c3041eb09a1dbef2f2e92f9e2d7696d747db6747 Mon Sep 17 00:00:00 2001 From: Meadowsys Date: Wed, 13 Sep 2023 20:48:56 -0700 Subject: [PATCH 11/17] it loads core strings now yay --- src/i18n.js | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/i18n.js b/src/i18n.js index b5d7b4ce53..4ec4235286 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -1,9 +1,25 @@ const fs = require("fs-plus"); const path = require("path"); +const season = require("season"); const { default: IntlMessageFormat } = require("intl-messageformat"); const { parse: parseToAST } = require("@formatjs/icu-messageformat-parser"); -const supportedFileExts = ["cson", "json"]; +/** + * @type {Array<{ + * ext: string; + * parse: (s: string) => any + * }>} + */ +const supportedFileExts = [ + { + ext: "cson", + parse: str => season.parse(str) + }, + { + ext: "json", + parse: str => JSON.parse(str) + } +]; module.exports = class I18n { /** @@ -58,7 +74,9 @@ module.exports = class I18n { this.loadStringsForCore(); } - loadStringsForCore() {} + loadStringsForCore() { + this._loadStringsAt("core", path.join(this.resourcePath, "i18n")); + } loadStringsForPackage() {} @@ -67,6 +85,7 @@ module.exports = class I18n { * @param {string} i18nDirPath path to the i18n dir with the files in it */ _loadStringsAt(pkgName, i18nDirPath) { + if (!fs.existsSync(i18nDirPath)) return; /** @type {Array} */ const filesArray = fs.readdirSync(i18nDirPath); const files = new Set(filesArray.map(f => f.toLowerCase())); @@ -74,14 +93,14 @@ module.exports = class I18n { /** @type {PackageStrings} */ const packageStrings = {}; this.locales.forEach(locale => { - const ext = supportedFileExts.find(ext => files.has(`${locale}.${ext}`)); + const ext = supportedFileExts.find(({ ext }) => files.has(`${locale}.${ext}`)); if (!ext) return; - const filename = `${locale}.${ext}`; + const filename = `${locale}.${ext.ext}`; const filepath = path.join(i18nDirPath, filename); const strings = fs.readFileSync(filepath, "utf8"); - packageStrings[locale] = strings; + packageStrings[locale] = ext.parse(strings); }); this.localisations.addPackage({ pkgName, From 0c29ce7f66a291e6ca1d9b32c093d7ca01148f9d Mon Sep 17 00:00:00 2001 From: Meadowsys Date: Wed, 13 Sep 2023 22:32:41 -0700 Subject: [PATCH 12/17] add t and load package strings --- src/i18n.js | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/i18n.js b/src/i18n.js index 4ec4235286..204e4b10f1 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -71,14 +71,29 @@ module.exports = class I18n { this.localisations.initialise({ locales: this.locales }); // TODO ast cache - this.loadStringsForCore(); + this._loadStringsForCore(); } - loadStringsForCore() { + /** + * @param {Key} keystr + * @param {Opts} opts + */ + t(keystr, opts = {}) { + return this.localisations.t(keystr, opts); + } + + _loadStringsForCore() { this._loadStringsAt("core", path.join(this.resourcePath, "i18n")); } - loadStringsForPackage() {} + /** + * @param {object} obj + * @param {string} obj.pkgName + * @param {string} obj.pkgPath + */ + loadStringsForPackage({ pkgName, pkgPath }) { + this._loadStringsAt(pkgName, path.join(pkgPath, "i18n")); + } /** * @param {string} pkgName @@ -86,8 +101,10 @@ module.exports = class I18n { */ _loadStringsAt(pkgName, i18nDirPath) { if (!fs.existsSync(i18nDirPath)) return; + /** @type {Array} */ const filesArray = fs.readdirSync(i18nDirPath); + // set search performance is supposed to be better than array const files = new Set(filesArray.map(f => f.toLowerCase())); /** @type {PackageStrings} */ @@ -186,8 +203,11 @@ class PackageLocalisations { * @param {Opts} opts */ t(keystr, opts = {}) { + const key = keystr.split("."); + guardPrototypePollution(key); + for (const locale of this.locales) { - const localised = this.localeObjs[locale]?.t(keystr, opts); + const localised = this.localeObjs[locale]?.t(key, opts); if (localised) return localised; } } @@ -214,13 +234,10 @@ class SingleLanguageLocalisations { } /** - * @param {Key} keystr + * @param {SplitKey} key * @param {Opts} opts */ - t(keystr, opts = {}) { - const key = keystr.split("."); - guardPrototypePollution(key); - + t(key, opts = {}) { const formatter = this._getFormatter(key); if (formatter) { const formatted = /** @type {string} */ (formatter.format(opts)); From 99e8130afc6c3b84428eaaa3646dadf1a938a8f9 Mon Sep 17 00:00:00 2001 From: Meadowsys Date: Wed, 13 Sep 2023 23:26:03 -0700 Subject: [PATCH 13/17] =?UTF-8?q?Stuff=E2=84=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/i18n.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n.js b/src/i18n.js index 204e4b10f1..e9854b90a1 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -41,7 +41,7 @@ module.exports = class I18n { type: "string", order: 1, default: "en", - // TODO get available languages for enum + // TODO get available languages for enum purposes }, fallbackLanguages: { type: "array", @@ -54,7 +54,7 @@ module.exports = class I18n { } } } - }) + }); } /** From de474d09233699f550fa9b560e274a74c61336d0 Mon Sep 17 00:00:00 2001 From: Meadowsys Date: Thu, 14 Sep 2023 14:02:50 -0700 Subject: [PATCH 14/17] now every package obj has a reference to i18n still not broken! --- src/atom-environment.js | 3 ++- src/package-manager.js | 9 ++++++--- src/package.js | 1 + 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/atom-environment.js b/src/atom-environment.js index 48a922131c..e9c389f7c4 100644 --- a/src/atom-environment.js +++ b/src/atom-environment.js @@ -140,7 +140,8 @@ class AtomEnvironment { grammarRegistry: this.grammars, deserializerManager: this.deserializers, viewRegistry: this.views, - uriHandlerRegistry: this.uriHandlerRegistry + uriHandlerRegistry: this.uriHandlerRegistry, + i18n: this.i18n }); /** @type {ThemeManager} */ diff --git a/src/package-manager.js b/src/package-manager.js index 23a8e904dc..372276357c 100644 --- a/src/package-manager.js +++ b/src/package-manager.js @@ -38,7 +38,8 @@ module.exports = class PackageManager { grammarRegistry: this.grammarRegistry, deserializerManager: this.deserializerManager, viewRegistry: this.viewRegistry, - uriHandlerRegistry: this.uriHandlerRegistry + uriHandlerRegistry: this.uriHandlerRegistry, + i18n: this.i18n } = params); this.emitter = new Emitter(); @@ -587,7 +588,8 @@ module.exports = class PackageManager { menuManager: this.menuManager, contextMenuManager: this.contextMenuManager, deserializerManager: this.deserializerManager, - viewRegistry: this.viewRegistry + viewRegistry: this.viewRegistry, + i18n: this.i18n }; pack = metadata.theme ? new ThemePackage(options) : new Package(options); @@ -692,7 +694,8 @@ module.exports = class PackageManager { menuManager: this.menuManager, contextMenuManager: this.contextMenuManager, deserializerManager: this.deserializerManager, - viewRegistry: this.viewRegistry + viewRegistry: this.viewRegistry, + i18n: this.i18n }; const pack = metadata.theme diff --git a/src/package.js b/src/package.js index 0071472dc2..a69732aa29 100644 --- a/src/package.js +++ b/src/package.js @@ -30,6 +30,7 @@ module.exports = class Package { this.contextMenuManager = params.contextMenuManager; this.deserializerManager = params.deserializerManager; this.viewRegistry = params.viewRegistry; + this.i18n = params.i18n; this.emitter = new Emitter(); this.mainModule = null; From 87adc66da936ca5ee323636f7e4a36f88eae3752 Mon Sep 17 00:00:00 2001 From: Meadowsys Date: Thu, 14 Sep 2023 15:38:43 -0700 Subject: [PATCH 15/17] put a t on every package will this be useful? we'll see i suppose --- spec/package-spec.js | 3 ++- src/i18n.js | 11 +++++++++++ src/package.js | 1 + 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/spec/package-spec.js b/spec/package-spec.js index a235c4e919..899a4c9b02 100644 --- a/spec/package-spec.js +++ b/spec/package-spec.js @@ -18,7 +18,8 @@ describe('Package', function() { menuManager: atom.menu, contextMenuManager: atom.contextMenu, deserializerManager: atom.deserializers, - viewRegistry: atom.views + viewRegistry: atom.views, + i18n: atom.i18n }); const buildPackage = packagePath => build(Package, packagePath); diff --git a/src/i18n.js b/src/i18n.js index e9854b90a1..1a7d936893 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -82,6 +82,17 @@ module.exports = class I18n { return this.localisations.t(keystr, opts); } + /** + * @param {string} ns + */ + getT(ns) { + /** + * @param {Key} keystr + * @param {Opts} opts + */ + return (keystr, opts = {}) => this.t(`${ns}.${keystr}`, opts); + } + _loadStringsForCore() { this._loadStringsAt("core", path.join(this.resourcePath, "i18n")); } diff --git a/src/package.js b/src/package.js index a69732aa29..17415eb1d7 100644 --- a/src/package.js +++ b/src/package.js @@ -46,6 +46,7 @@ module.exports = class Package { (this.metadata && this.metadata.name) || params.name || path.basename(this.path); + this.t = this.i18n.getT(this.name); this.reset(); } From 97173770d2400d286451a81d5f8ab7c68c64294d Mon Sep 17 00:00:00 2001 From: Meadowsys Date: Mon, 18 Sep 2023 15:52:48 +0100 Subject: [PATCH 16/17] put that t into package activate call ~~hopefully~~ --- spec/package-manager-spec.js | 2 +- src/package.js | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/spec/package-manager-spec.js b/spec/package-manager-spec.js index 6948e10eb1..aca17056a9 100644 --- a/spec/package-manager-spec.js +++ b/spec/package-manager-spec.js @@ -1019,7 +1019,7 @@ describe('PackageManager', () => { spyOn(pack.mainModule, 'activate').andCallThrough(); await atom.packages.activatePackage('package-with-serialization'); - expect(pack.mainModule.activate).toHaveBeenCalledWith({ someNumber: 77 }); + expect(pack.mainModule.activate).toHaveBeenCalledWith({ someNumber: 77 }, { t: pack.t }); }); it('invokes ::onDidActivatePackage listeners with the activated package', async () => { diff --git a/src/package.js b/src/package.js index 17415eb1d7..db9534c6ad 100644 --- a/src/package.js +++ b/src/package.js @@ -242,7 +242,10 @@ module.exports = class Package { } if (typeof this.mainModule.activate === 'function') { this.mainModule.activate( - this.packageManager.getPackageState(this.name) || {} + this.packageManager.getPackageState(this.name) || {}, + { + t: this.t + } ); } this.mainActivated = true; From 1449ea7f9f2abf1d716935f6acb6c50effcf320e Mon Sep 17 00:00:00 2001 From: Meadowsys Date: Wed, 11 Oct 2023 20:48:50 -0700 Subject: [PATCH 17/17] split key and guard proto pollution earlier --- src/i18n.js | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/i18n.js b/src/i18n.js index 1a7d936893..950730c393 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -156,16 +156,18 @@ class Localisations { } /** - * @param {Key} _keystr + * @param {Key} keystr * @param {Opts} opts */ - t(_keystr, opts = {}) { - const i = _keystr.indexOf("."); - if (i < 0) return fallback(_keystr, opts); + t(keystr, opts = {}) { + let key = keystr.split("."); + if (key.length < 2) return fallback(keystr, opts); - const pkgName = _keystr.substring(0, i); - const keystr = _keystr.substring(i + 1); - return this.packages[pkgName]?.t(keystr, opts) ?? fallback(_keystr, opts); + guardPrototypePollution(key); + + const pkgName = key[0]; + key = key.slice(1); + return this.packages[pkgName]?.t(key, opts) ?? fallback(keystr, opts); } /** @@ -210,13 +212,10 @@ class PackageLocalisations { } /** - * @param {Key} keystr + * @param {SplitKey} key * @param {Opts} opts */ - t(keystr, opts = {}) { - const key = keystr.split("."); - guardPrototypePollution(key); - + t(key, opts = {}) { for (const locale of this.locales) { const localised = this.localeObjs[locale]?.t(key, opts); if (localised) return localised;