scratch-l10n 5.0.309 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/README.md +6 -0
- package/dist/l10n.js +3 -0
- package/dist/l10n.js.map +1 -1
- package/dist/localeData.js +3 -0
- package/dist/localeData.js.map +1 -1
- package/dist/supportedLocales.js +3 -0
- package/dist/supportedLocales.js.map +1 -1
- package/package.json +23 -21
- package/scripts/{build-data.mjs → build-data.mts} +15 -6
- package/scripts/{build-i18n-src.js → build-i18n-src.mts} +16 -11
- package/scripts/lib/concurrent.mts +37 -0
- package/scripts/lib/freshdesk-api.mts +322 -0
- package/scripts/lib/help-utils.mts +221 -0
- package/{lib/progress-logger.mjs → scripts/lib/progress-logger.mts} +10 -5
- package/scripts/lib/transifex-formats.mts +53 -0
- package/scripts/lib/transifex-objects.mts +143 -0
- package/scripts/lib/transifex.mts +284 -0
- package/scripts/lib/validate.mts +107 -0
- package/scripts/tsconfig.json +20 -0
- package/scripts/tx-pull-editor.mts +74 -0
- package/scripts/{tx-pull-help-articles.js → tx-pull-help-articles.mts} +5 -13
- package/scripts/{tx-pull-help-names.js → tx-pull-help-names.mts} +5 -13
- package/scripts/{tx-pull-locale-articles.js → tx-pull-locale-articles.mts} +5 -13
- package/scripts/{tx-pull-www.mjs → tx-pull-www.mts} +16 -29
- package/scripts/{tx-push-help.mjs → tx-push-help.mts} +39 -37
- package/scripts/{tx-push-src.js → tx-push-src.mts} +13 -20
- package/scripts/update-translations.sh +2 -2
- package/scripts/{validate-extension-inputs.mjs → validate-extension-inputs.mts} +20 -10
- package/scripts/{validate-translations.mjs → validate-translations.mts} +7 -12
- package/scripts/{validate-www.mjs → validate-www.mts} +15 -13
- package/src/supported-locales.mjs +3 -0
- package/www/scratch-website.about-l10njson/nn.json +1 -1
- package/www/scratch-website.splash-l10njson/mi.json +2 -2
- package/www/scratch-website.teacher-faq-l10njson/cy.json +1 -1
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -75
- package/.github/workflows/ci-cd.yml +0 -55
- package/.github/workflows/commitlint.yml +0 -12
- package/.github/workflows/daily-help-update.yml +0 -40
- package/.github/workflows/daily-tx-pull.yml +0 -54
- package/.github/workflows/signature-assistant.yml +0 -31
- package/lib/batch.js +0 -15
- package/lib/transifex.js +0 -242
- package/lib/validate.mjs +0 -48
- package/scripts/freshdesk-api.js +0 -149
- package/scripts/help-utils.js +0 -190
- package/scripts/tx-pull-editor.mjs +0 -83
package/dist/supportedLocales.js
CHANGED
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"supportedLocales.js","sources":["webpack:///webpack/bootstrap","webpack:///./src/supported-locales.mjs"],"sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = \"./src/supported-locales.mjs\");\n","/**\n * Currently supported locales for the Scratch Project\n * @type {object} Key Value pairs of locale code: Language name written in the language\n */\n\nconst locales = {\n ab: { name: 'Аҧсшәа' },\n af: { name: 'Afrikaans' },\n ar: { name: 'العربية' },\n am: { name: 'አማርኛ' },\n an: { name: 'Aragonés' },\n ast: { name: 'Asturianu' },\n az: { name: 'Azeri' },\n id: { name: 'Bahasa Indonesia' },\n bn: { name: 'বাংলা' },\n be: { name: 'Беларуская' },\n bg: { name: 'Български' },\n ca: { name: 'Català' },\n cs: { name: 'Česky' },\n cy: { name: 'Cymraeg' },\n da: { name: 'Dansk' },\n de: { name: 'Deutsch' },\n et: { name: 'Eesti' },\n el: { name: 'Ελληνικά' },\n en: { name: 'English' },\n es: { name: 'Español (España)' },\n 'es-419': { name: 'Español Latinoamericano' },\n eo: { name: 'Esperanto' },\n eu: { name: 'Euskara' },\n fa: { name: 'فارسی' },\n fil: { name: 'Filipino' },\n fr: { name: 'Français' },\n fy: { name: 'Frysk' },\n ga: { name: 'Gaeilge' },\n gd: { name: 'Gàidhlig' },\n gl: { name: 'Galego' },\n ko: { name: '한국어' },\n ha: { name: 'Hausa' },\n hy: { name: 'Հայերեն' },\n he: { name: 'עִבְרִית' },\n hi: { name: 'हिंदी' },\n hr: { name: 'Hrvatski' },\n xh: { name: 'isiXhosa' },\n zu: { name: 'isiZulu' },\n is: { name: 'Íslenska' },\n it: { name: 'Italiano' },\n ka: { name: 'ქართული ენა' },\n kk: { name: 'қазақша' },\n qu: { name: 'Kichwa' },\n sw: { name: 'Kiswahili' },\n ht: { name: 'Kreyòl ayisyen' },\n ku: { name: 'Kurdî' },\n ckb: { name: 'کوردیی ناوەندی' },\n lv: { name: 'Latviešu' },\n lt: { name: 'Lietuvių' },\n hu: { name: 'Magyar' },\n mi: { name: 'Māori' },\n mn: { name: 'Монгол хэл' },\n nl: { name: 'Nederlands' },\n ja: { name: '日本語' },\n 'ja-Hira': { name: 'にほんご' },\n nb: { name: 'Norsk Bokmål' },\n nn: { name: 'Norsk Nynorsk' },\n oc: { name: 'Occitan' },\n or: { name: 'ଓଡ଼ିଆ' },\n uz: { name: 'Oʻzbekcha' },\n th: { name: 'ไทย' },\n km: { name: 'ភាសាខ្មែរ' },\n pl: { name: 'Polski' },\n pt: { name: 'Português' },\n 'pt-br': { name: 'Português Brasileiro' },\n rap: { name: 'Rapa Nui' },\n ro: { name: 'Română' },\n ru: { name: 'Русский' },\n nso: { name: 'Sepedi' },\n tn: { name: 'Setswana' },\n sk: { name: 'Slovenčina' },\n sl: { name: 'Slovenščina' },\n sr: { name: 'Српски' },\n fi: { name: 'Suomi' },\n sv: { name: 'Svenska' },\n vi: { name: 'Tiếng Việt' },\n tr: { name: 'Türkçe' },\n uk: { name: 'Українська' },\n 'zh-cn': { name: '简体中文' },\n 'zh-tw': { name: '繁體中文' },\n}\n\nconst customLocales = {\n ab: {\n locale: 'ab',\n parentLocale: 'ru',\n },\n // Aragonese is not in the locale data, using es for Spain\n an: {\n locale: 'an',\n parentLocale: 'es',\n },\n // haitian creole is not in locale-langData\n ht: {\n locale: 'ht',\n parentLocale: 'fr',\n },\n oc: {\n locale: 'oc',\n parentLocale: 'fr',\n },\n rap: {\n locale: 'rap',\n parentLocale: 'es',\n },\n // TODO: replace zh-cn, zh-tw with zh-Hans and zh-Hant then customLocales is unnecessary\n 'zh-cn': {\n locale: 'zh-cn',\n parentLocale: 'zh',\n },\n 'zh-tw': {\n locale: 'zh-tw',\n parentLocale: 'zh',\n },\n}\n\nconst localeMap = {\n 'aa-dj': 'aa_DJ',\n 'es-419': 'es_419',\n // ja-Hira: no map - it's 'ja-Hira' on transifex\n 'pt-br': 'pt_BR',\n 'zh-cn': 'zh_CN',\n 'zh-tw': 'zh_TW',\n}\n\n// list of RTL locales supported, and a function to check whether a locale is RTL\nconst rtlLocales = ['ar', 'ckb', 'fa', 'he']\n\nconst isRtl = locale => rtlLocales.indexOf(locale) !== -1\n\nexport { locales as default, customLocales, localeMap, isRtl }\n"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;AClFA;AAAA;AAAA;AAAA;AAAA;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;A","sourceRoot":""}
|
1
|
+
{"version":3,"file":"supportedLocales.js","sources":["webpack:///webpack/bootstrap","webpack:///./src/supported-locales.mjs"],"sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = \"./src/supported-locales.mjs\");\n","/**\n * Currently supported locales for the Scratch Project\n * @type {object} Key Value pairs of locale code: Language name written in the language\n */\n\nconst locales = {\n ab: { name: 'Аҧсшәа' },\n af: { name: 'Afrikaans' },\n ar: { name: 'العربية' },\n am: { name: 'አማርኛ' },\n an: { name: 'Aragonés' },\n ast: { name: 'Asturianu' },\n az: { name: 'Azeri' },\n id: { name: 'Bahasa Indonesia' },\n bn: { name: 'বাংলা' },\n be: { name: 'Беларуская' },\n bg: { name: 'Български' },\n ca: { name: 'Català' },\n cs: { name: 'Česky' },\n cy: { name: 'Cymraeg' },\n da: { name: 'Dansk' },\n de: { name: 'Deutsch' },\n et: { name: 'Eesti' },\n el: { name: 'Ελληνικά' },\n en: { name: 'English' },\n es: { name: 'Español (España)' },\n 'es-419': { name: 'Español Latinoamericano' },\n eo: { name: 'Esperanto' },\n eu: { name: 'Euskara' },\n fa: { name: 'فارسی' },\n fil: { name: 'Filipino' },\n fr: { name: 'Français' },\n fy: { name: 'Frysk' },\n ga: { name: 'Gaeilge' },\n gd: { name: 'Gàidhlig' },\n gl: { name: 'Galego' },\n ko: { name: '한국어' },\n ha: { name: 'Hausa' },\n hy: { name: 'Հայերեն' },\n he: { name: 'עִבְרִית' },\n hi: { name: 'हिंदी' },\n hr: { name: 'Hrvatski' },\n xh: { name: 'isiXhosa' },\n zu: { name: 'isiZulu' },\n is: { name: 'Íslenska' },\n it: { name: 'Italiano' },\n ka: { name: 'ქართული ენა' },\n kk: { name: 'қазақша' },\n qu: { name: 'Kichwa' },\n sw: { name: 'Kiswahili' },\n ht: { name: 'Kreyòl ayisyen' },\n ku: { name: 'Kurdî' },\n ckb: { name: 'کوردیی ناوەندی' },\n lv: { name: 'Latviešu' },\n lt: { name: 'Lietuvių' },\n hu: { name: 'Magyar' },\n mi: { name: 'Māori' },\n mn: { name: 'Монгол хэл' },\n nl: { name: 'Nederlands' },\n ja: { name: '日本語' },\n 'ja-Hira': { name: 'にほんご' },\n nb: { name: 'Norsk Bokmål' },\n nn: { name: 'Norsk Nynorsk' },\n oc: { name: 'Occitan' },\n or: { name: 'ଓଡ଼ିଆ' },\n uz: { name: 'Oʻzbekcha' },\n th: { name: 'ไทย' },\n km: { name: 'ភាសាខ្មែរ' },\n pl: { name: 'Polski' },\n pt: { name: 'Português' },\n 'pt-br': { name: 'Português Brasileiro' },\n rap: { name: 'Rapa Nui' },\n ro: { name: 'Română' },\n ru: { name: 'Русский' },\n nso: { name: 'Sepedi' },\n tn: { name: 'Setswana' },\n sk: { name: 'Slovenčina' },\n sl: { name: 'Slovenščina' },\n sr: { name: 'Српски' },\n fi: { name: 'Suomi' },\n sv: { name: 'Svenska' },\n vi: { name: 'Tiếng Việt' },\n tr: { name: 'Türkçe' },\n uk: { name: 'Українська' },\n 'zh-cn': { name: '简体中文' },\n 'zh-tw': { name: '繁體中文' },\n}\n\nconst customLocales = {\n ab: {\n locale: 'ab',\n parentLocale: 'ru',\n },\n // Aragonese is not in the locale data, using es for Spain\n an: {\n locale: 'an',\n parentLocale: 'es',\n },\n // haitian creole is not in locale-langData\n ht: {\n locale: 'ht',\n parentLocale: 'fr',\n },\n oc: {\n locale: 'oc',\n parentLocale: 'fr',\n },\n rap: {\n locale: 'rap',\n parentLocale: 'es',\n },\n // TODO: replace zh-cn, zh-tw with zh-Hans and zh-Hant then customLocales is unnecessary\n 'zh-cn': {\n locale: 'zh-cn',\n parentLocale: 'zh',\n },\n 'zh-tw': {\n locale: 'zh-tw',\n parentLocale: 'zh',\n },\n}\n\n/**\n * @type {Record<string,string>}\n */\nconst localeMap = {\n 'aa-dj': 'aa_DJ',\n 'es-419': 'es_419',\n // ja-Hira: no map - it's 'ja-Hira' on transifex\n 'pt-br': 'pt_BR',\n 'zh-cn': 'zh_CN',\n 'zh-tw': 'zh_TW',\n}\n\n// list of RTL locales supported, and a function to check whether a locale is RTL\nconst rtlLocales = ['ar', 'ckb', 'fa', 'he']\n\nconst isRtl = locale => rtlLocales.indexOf(locale) !== -1\n\nexport { locales as default, customLocales, localeMap, isRtl }\n"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;AClFA;AAAA;AAAA;AAAA;AAAA;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;A","sourceRoot":""}
|
package/package.json
CHANGED
@@ -1,41 +1,41 @@
|
|
1
1
|
{
|
2
2
|
"name": "scratch-l10n",
|
3
|
-
"version": "
|
3
|
+
"version": "6.0.0",
|
4
4
|
"description": "Localization for the Scratch 3.0 components",
|
5
5
|
"main": "./dist/l10n.js",
|
6
6
|
"browser": "./src/index.mjs",
|
7
7
|
"bin": {
|
8
|
-
"build-i18n-src": "./scripts/build-i18n-src.
|
9
|
-
"tx-push-src": "./scripts/tx-push-src.
|
8
|
+
"build-i18n-src": "./scripts/build-i18n-src.mts",
|
9
|
+
"tx-push-src": "./scripts/tx-push-src.mts"
|
10
10
|
},
|
11
11
|
"scripts": {
|
12
12
|
"build": "npm run clean && npm run build:data && webpack --progress --colors --bail",
|
13
|
-
"build:data": "
|
13
|
+
"build:data": "./scripts/build-data.mts",
|
14
14
|
"clean": "rimraf ./dist ./locales && mkdirp dist locales",
|
15
15
|
"format": "prettier --write . && eslint --fix",
|
16
16
|
"lint": "npm run lint:js && npm run lint:json",
|
17
17
|
"lint:js": "eslint && prettier --check .",
|
18
18
|
"lint:json": "jshint -e .json www editor/blocks editor/extensions editor/interface editor/paint-editor",
|
19
19
|
"prepare": "husky install",
|
20
|
-
"pull:blocks": "
|
20
|
+
"pull:blocks": "./scripts/tx-pull-editor.mts scratch-editor blocks ./editor/blocks/",
|
21
21
|
"pull:editor": "npm run pull:blocks && npm run pull:extensions && npm run pull:paint && npm run pull:interface",
|
22
|
-
"pull:extensions": "
|
22
|
+
"pull:extensions": "./scripts/tx-pull-editor.mts scratch-editor extensions ./editor/extensions/",
|
23
23
|
"pull:help": "npm run pull:help:names && npm run pull:help:articles",
|
24
|
-
"pull:help:articles": "./scripts/tx-pull-help-articles.
|
25
|
-
"pull:help:names": "./scripts/tx-pull-help-names.
|
26
|
-
"pull:interface": "
|
27
|
-
"pull:paint": "
|
28
|
-
"pull:www": "
|
29
|
-
"push:help": "./scripts/tx-push-help.
|
24
|
+
"pull:help:articles": "./scripts/tx-pull-help-articles.mts",
|
25
|
+
"pull:help:names": "./scripts/tx-pull-help-names.mts",
|
26
|
+
"pull:interface": "./scripts/tx-pull-editor.mts scratch-editor interface ./editor/interface/",
|
27
|
+
"pull:paint": "./scripts/tx-pull-editor.mts scratch-editor paint-editor ./editor/paint-editor/",
|
28
|
+
"pull:www": "./scripts/tx-pull-www.mts ./www",
|
29
|
+
"push:help": "./scripts/tx-push-help.mts",
|
30
30
|
"sync:help": "npm run push:help && npm run pull:help",
|
31
31
|
"test": "npm run lint:js && npm run validate:editor && npm run validate:www && npm run build && npm run lint:json",
|
32
32
|
"update": "scripts/update-translations.sh",
|
33
|
-
"validate:blocks": "
|
33
|
+
"validate:blocks": "./scripts/validate-translations.mts ./editor/blocks/",
|
34
34
|
"validate:editor": "npm run validate:blocks && npm run validate:extensions && npm run validate:interface && npm run validate:paint",
|
35
|
-
"validate:extensions": "
|
36
|
-
"validate:interface": "
|
37
|
-
"validate:paint": "
|
38
|
-
"validate:www": "
|
35
|
+
"validate:extensions": "./scripts/validate-translations.mts ./editor/extensions/ && ./scripts/validate-extension-inputs.mts",
|
36
|
+
"validate:interface": "./scripts/validate-translations.mts ./editor/interface/",
|
37
|
+
"validate:paint": "./scripts/validate-translations.mts ./editor/paint-editor/",
|
38
|
+
"validate:www": "./scripts/validate-www.mts ./www"
|
39
39
|
},
|
40
40
|
"repository": {
|
41
41
|
"type": "git",
|
@@ -49,8 +49,8 @@
|
|
49
49
|
"homepage": "https://github.com/scratchfoundation/scratch-l10n#readme",
|
50
50
|
"dependencies": {
|
51
51
|
"@transifex/api": "7.1.4",
|
52
|
-
"
|
53
|
-
"
|
52
|
+
"transifex": "1.6.6",
|
53
|
+
"tsx": "4.19.4"
|
54
54
|
},
|
55
55
|
"devDependencies": {
|
56
56
|
"@babel/cli": "7.28.0",
|
@@ -63,6 +63,9 @@
|
|
63
63
|
"@babel/preset-react": "7.27.1",
|
64
64
|
"@commitlint/cli": "18.6.1",
|
65
65
|
"@commitlint/config-conventional": "18.6.3",
|
66
|
+
"@types/async": "3.2.24",
|
67
|
+
"@types/glob": "8.1.0",
|
68
|
+
"@types/lodash.defaultsdeep": "4.6.9",
|
66
69
|
"async": "3.2.6",
|
67
70
|
"babel-loader": "8.4.1",
|
68
71
|
"babel-plugin-react-intl": "3.5.1",
|
@@ -77,8 +80,7 @@
|
|
77
80
|
"json": "^9.0.6",
|
78
81
|
"jsonlint": "1.6.3",
|
79
82
|
"lodash.defaultsdeep": "4.6.1",
|
80
|
-
"mkdirp": "0.
|
81
|
-
"node-fetch": "2.7.0",
|
83
|
+
"mkdirp": "3.0.1",
|
82
84
|
"p-limit": "2.3.0",
|
83
85
|
"p-queue": "3.2.0",
|
84
86
|
"prettier": "3.5.3",
|
@@ -1,4 +1,4 @@
|
|
1
|
-
#!/usr/bin/env
|
1
|
+
#!/usr/bin/env tsx
|
2
2
|
/*
|
3
3
|
Generates locales/<component>-msgs.js for each component (gui, etc) from the
|
4
4
|
current translation files for each language for that component
|
@@ -39,18 +39,27 @@ Missing locales are ignored, react-intl will use the default messages for them.
|
|
39
39
|
*/
|
40
40
|
import * as fs from 'fs'
|
41
41
|
import defaultsDeep from 'lodash.defaultsdeep'
|
42
|
-
import mkdirp from 'mkdirp'
|
42
|
+
import { mkdirp } from 'mkdirp'
|
43
43
|
import * as path from 'path'
|
44
44
|
import locales from '../src/supported-locales.mjs'
|
45
|
+
import { TransifexEditorStrings } from './lib/validate.mts'
|
45
46
|
|
46
47
|
const MSGS_DIR = './locales/'
|
47
48
|
mkdirp.sync(MSGS_DIR)
|
48
|
-
const missingLocales = []
|
49
49
|
|
50
|
-
const
|
51
|
-
|
50
|
+
const missingLocales: string[] = []
|
51
|
+
|
52
|
+
/**
|
53
|
+
* For a given component, load translation data from multiple locales and merge all of that into one translation map.
|
54
|
+
* @param component - the name of an editor component
|
55
|
+
* @returns translation data for the requested component
|
56
|
+
*/
|
57
|
+
const combineJson = (component: string) =>
|
58
|
+
Object.keys(locales).reduce((collection: Record<string, TransifexEditorStrings>, lang) => {
|
52
59
|
try {
|
53
|
-
const langData = JSON.parse(
|
60
|
+
const langData = JSON.parse(
|
61
|
+
fs.readFileSync(path.resolve('editor', component, lang + '.json'), 'utf8'),
|
62
|
+
) as TransifexEditorStrings
|
54
63
|
collection[lang] = langData
|
55
64
|
} catch {
|
56
65
|
missingLocales.push(component + ':' + lang + '\n')
|
@@ -1,11 +1,11 @@
|
|
1
|
-
#!/usr/bin/env
|
1
|
+
#!/usr/bin/env tsx
|
2
|
+
import fs from 'fs'
|
3
|
+
import glob from 'glob'
|
4
|
+
import { mkdirp } from 'mkdirp'
|
5
|
+
import path from 'path'
|
6
|
+
import { TransifexStringChrome, TransifexStrings } from './lib/transifex-formats.mts'
|
2
7
|
|
3
|
-
const
|
4
|
-
const glob = require('glob')
|
5
|
-
const path = require('path')
|
6
|
-
const mkdirp = require('mkdirp')
|
7
|
-
|
8
|
-
var args = process.argv.slice(2)
|
8
|
+
const args = process.argv.slice(2)
|
9
9
|
|
10
10
|
if (!args.length) {
|
11
11
|
process.stdout.write('You must specify the messages dir generated by babel-plugin-react-intl.\n')
|
@@ -14,12 +14,17 @@ if (!args.length) {
|
|
14
14
|
|
15
15
|
const MESSAGES_PATTERN = args.shift() + '/**/*.json'
|
16
16
|
|
17
|
-
|
17
|
+
const LANG_DIR = args.shift()
|
18
|
+
if (!LANG_DIR) {
|
18
19
|
process.stdout.write('A destination directory must be specified.\n')
|
19
20
|
process.exit(1)
|
20
21
|
}
|
21
22
|
|
22
|
-
|
23
|
+
interface Descriptor {
|
24
|
+
id: string
|
25
|
+
description: string
|
26
|
+
defaultMessage: string
|
27
|
+
}
|
23
28
|
|
24
29
|
// Aggregates the default messages that were extracted from the example app's
|
25
30
|
// React components via the React Intl Babel plugin. An error will be thrown if
|
@@ -29,8 +34,8 @@ const LANG_DIR = args.shift()
|
|
29
34
|
const defaultMessages = glob
|
30
35
|
.sync(MESSAGES_PATTERN)
|
31
36
|
.map(filename => fs.readFileSync(filename, 'utf8'))
|
32
|
-
.map(file => JSON.parse(file))
|
33
|
-
.reduce((collection
|
37
|
+
.map(file => JSON.parse(file) as Descriptor[])
|
38
|
+
.reduce((collection: TransifexStrings<TransifexStringChrome>, descriptors) => {
|
34
39
|
descriptors.forEach(({ id, defaultMessage, description }) => {
|
35
40
|
if (Object.prototype.hasOwnProperty.call(collection, id)) {
|
36
41
|
throw new Error(`Duplicate message id: ${id}`)
|
@@ -0,0 +1,37 @@
|
|
1
|
+
/**
|
2
|
+
* @param item - An item to process
|
3
|
+
* @returns The result of processing the item
|
4
|
+
*/
|
5
|
+
type ProcessItemAsync<T, R> = (item: T) => Promise<R>
|
6
|
+
|
7
|
+
/**
|
8
|
+
* Maps each value of an array into an async function, then returns an array of the results.
|
9
|
+
* Up to `poolSize` calls to the async function are allowed to run concurrently.
|
10
|
+
* @param arr - Array of input values.
|
11
|
+
* @param poolSize - Number of calls to `func` allowed to run concurrently.
|
12
|
+
* If `poolSize` is less than 2, tasks will be run sequentially.
|
13
|
+
* @param func - Function to apply to each item in `arr`.
|
14
|
+
* @returns Results of `func` applied to each item in `arr`.
|
15
|
+
*/
|
16
|
+
export const poolMap = async <T, R>(arr: T[], poolSize: number, func: ProcessItemAsync<T, R>): Promise<R[]> => {
|
17
|
+
const pool: Record<number, Promise<void>> = []
|
18
|
+
const results: R[] = []
|
19
|
+
|
20
|
+
const processItem = async (i: number): Promise<void> => {
|
21
|
+
results[i] = await func(arr[i])
|
22
|
+
delete pool[i]
|
23
|
+
}
|
24
|
+
|
25
|
+
for (let i = 0; i < arr.length; i++) {
|
26
|
+
pool[i] = processItem(i) // No `await` here: we're storing the promise, not the result
|
27
|
+
if (Object.keys(pool).length >= poolSize) {
|
28
|
+
// Wait for one of the promises in the pool to resolve
|
29
|
+
// and clear itself to make room for the next one
|
30
|
+
await Promise.race(Object.values(pool))
|
31
|
+
}
|
32
|
+
}
|
33
|
+
|
34
|
+
// Wait for any remaining promises to resolve
|
35
|
+
await Promise.all(Object.values(pool))
|
36
|
+
return results
|
37
|
+
}
|
@@ -0,0 +1,322 @@
|
|
1
|
+
// interface to FreshDesk Solutions (knowledge base) api
|
2
|
+
|
3
|
+
/**
|
4
|
+
* Extend
|
5
|
+
*/
|
6
|
+
export interface HttpError extends Error {
|
7
|
+
/** HTTP error code (if any) */
|
8
|
+
code: number | null | undefined
|
9
|
+
|
10
|
+
/** Retry-After header value (if any) */
|
11
|
+
retryAfter: string | null | undefined
|
12
|
+
}
|
13
|
+
|
14
|
+
/**
|
15
|
+
* Properties to provide when creating a Category throught the Freshdesk API. Also works for updates.
|
16
|
+
* @see https://developers.freshdesk.com/api/#create_solution_category
|
17
|
+
*/
|
18
|
+
export interface FreshdeskCategoryCreate {
|
19
|
+
/** Name of the solution category. Mandatory for Create. */
|
20
|
+
name: string
|
21
|
+
/** Description of the solution category */
|
22
|
+
description?: string
|
23
|
+
/** List of portal IDs where this category is visible */
|
24
|
+
visible_in_portals?: number[]
|
25
|
+
}
|
26
|
+
|
27
|
+
/**
|
28
|
+
* Categories broadly classify your solutions page into several sections.
|
29
|
+
* @see https://developers.freshdesk.com/api/#solution_category_attributes
|
30
|
+
*/
|
31
|
+
export interface FreshdeskCategory extends FreshdeskCategoryCreate {
|
32
|
+
/** Unique ID of the solution category */
|
33
|
+
id?: number
|
34
|
+
/** Solution Category creation timestamp */
|
35
|
+
created_at?: string
|
36
|
+
/** Solution Category update timestamp */
|
37
|
+
updated_at?: string
|
38
|
+
}
|
39
|
+
|
40
|
+
/**
|
41
|
+
* Properties to provide when creating a Folder throught the Freshdesk API. Also works for updates.
|
42
|
+
* @see https://developers.freshdesk.com/api/#create_solution_folder
|
43
|
+
*/
|
44
|
+
export interface FreshdeskFolderCreate {
|
45
|
+
/** Description of the solution folder */
|
46
|
+
description?: string
|
47
|
+
/** Name of the solution folder */
|
48
|
+
name: string
|
49
|
+
/** ID of the parent folder */
|
50
|
+
parent_folder_id?: number
|
51
|
+
/** Accessibility of this folder. Please refer to Folder Properties table. */
|
52
|
+
visibility?: FreshdeskFolderVisibility
|
53
|
+
/** IDs of the companies to whom this solution folder is visible */
|
54
|
+
company_ids?: number[]
|
55
|
+
/** IDs of the contact segments to whom this solution folder is visible */
|
56
|
+
contact_segment_ids?: number[]
|
57
|
+
/** IDs of the company segments to whom this solution folder is visible */
|
58
|
+
company_segment_ids?: number[]
|
59
|
+
}
|
60
|
+
|
61
|
+
/**
|
62
|
+
* Related Solutions Articles and/or Folders are organized into Folders. Folders make it convenient for users to read
|
63
|
+
* similar articles or navigate to other possible solutions to their problem.
|
64
|
+
* @see https://developers.freshdesk.com/api/#solution_folder_attributes
|
65
|
+
*/
|
66
|
+
export interface FreshdeskFolder extends FreshdeskFolderCreate {
|
67
|
+
/** Unique ID of the solution folder */
|
68
|
+
id?: number
|
69
|
+
/** Parent category and folders in which the folder is placed */
|
70
|
+
hierarchy?: object[]
|
71
|
+
/** Number of articles present inside a folder */
|
72
|
+
articles_count?: number
|
73
|
+
/** Number of folders present inside a folder */
|
74
|
+
sub_folders_count?: number
|
75
|
+
/** Solution Folder creation timestamp */
|
76
|
+
created_at?: string
|
77
|
+
/** Solution Folder updated timestamp */
|
78
|
+
updated_at?: string
|
79
|
+
}
|
80
|
+
|
81
|
+
export enum FreshdeskFolderVisibility {
|
82
|
+
AllUsers = 1,
|
83
|
+
LoggedInUsers = 2,
|
84
|
+
Agents = 3,
|
85
|
+
SelectedCompanies = 4,
|
86
|
+
Bots = 5,
|
87
|
+
SelectedContactSegments = 6,
|
88
|
+
SelectedCompanySegments = 7,
|
89
|
+
}
|
90
|
+
|
91
|
+
/**
|
92
|
+
* Properties to provide when creating an Article throught the Freshdesk API. Also works for updates.
|
93
|
+
* @see https://developers.freshdesk.com/api/#create_solution_article
|
94
|
+
*/
|
95
|
+
export interface FreshdeskArticleCreate {
|
96
|
+
/** ID of the agent who created the solution article */
|
97
|
+
agent_id?: number
|
98
|
+
/** Description of the solution article */
|
99
|
+
description: string
|
100
|
+
/** Status of the solution article */
|
101
|
+
status: FreshdeskArticleStatus
|
102
|
+
/** Meta data for search engine optimization. Allows meta_title, meta_description and meta_keywords */
|
103
|
+
seo_data?: FreshdeskSEOData
|
104
|
+
/** Tags that have been associated with the solution article */
|
105
|
+
tags?: string[]
|
106
|
+
/** Title of the solution article */
|
107
|
+
title: string
|
108
|
+
}
|
109
|
+
|
110
|
+
/**
|
111
|
+
* Solution Articles or knowledge base posts promote self-help in your support portal. These should ideally cover all
|
112
|
+
* aspects of your product or service like "how-to" instructions and FAQs.
|
113
|
+
* @see https://developers.freshdesk.com/api/#solution_article_attributes
|
114
|
+
*/
|
115
|
+
export interface FreshdeskArticle extends FreshdeskArticleCreate {
|
116
|
+
/** Unique ID of the solution article */
|
117
|
+
id?: number
|
118
|
+
/** ID of the category to which the solution article belongs */
|
119
|
+
category_id?: number
|
120
|
+
/** Description of the solution article in plain text */
|
121
|
+
description_text?: string
|
122
|
+
/** ID of the folder to which the solution article belongs */
|
123
|
+
folder_id?: number
|
124
|
+
/** Parent category and folders in which the article is placed */
|
125
|
+
hierarchy?: object[]
|
126
|
+
/** Number of views for the solution article */
|
127
|
+
hits?: number
|
128
|
+
/** Number of down votes for the solution article */
|
129
|
+
thumbs_down?: number
|
130
|
+
/** Number of upvotes for the solution article */
|
131
|
+
thumbs_up?: number
|
132
|
+
/** Solution Article creation timestamp */
|
133
|
+
created_at?: string
|
134
|
+
/** Solution Article updated timestamp */
|
135
|
+
updated_at?: string
|
136
|
+
}
|
137
|
+
|
138
|
+
export enum FreshdeskArticleStatus {
|
139
|
+
draft = 1,
|
140
|
+
published = 2,
|
141
|
+
}
|
142
|
+
|
143
|
+
export interface FreshdeskSEOData {
|
144
|
+
meta_title?: string
|
145
|
+
meta_description?: string
|
146
|
+
meta_keywords?: string[]
|
147
|
+
}
|
148
|
+
|
149
|
+
/**
|
150
|
+
* Wrapper for Freshdesk's REST API
|
151
|
+
*/
|
152
|
+
export class FreshdeskApi {
|
153
|
+
baseUrl: string
|
154
|
+
private _auth: string
|
155
|
+
defaultHeaders: { 'Content-Type': string; Authorization: string }
|
156
|
+
rateLimited: boolean
|
157
|
+
|
158
|
+
constructor(baseUrl: string, apiKey: string) {
|
159
|
+
this.baseUrl = baseUrl
|
160
|
+
this._auth = 'Basic ' + Buffer.from(`${apiKey}:X`).toString('base64')
|
161
|
+
this.defaultHeaders = {
|
162
|
+
'Content-Type': 'application/json',
|
163
|
+
Authorization: this._auth,
|
164
|
+
}
|
165
|
+
this.rateLimited = false
|
166
|
+
}
|
167
|
+
|
168
|
+
/**
|
169
|
+
* Checks the status of a response. If status is not ok, or the body is not JSON, raise exception
|
170
|
+
* @param res - The response object
|
171
|
+
* @returns the response if it is ok
|
172
|
+
*/
|
173
|
+
checkStatus(res: Response) {
|
174
|
+
if (res.ok) {
|
175
|
+
if (res.headers.get('content-type')?.includes('application/json')) {
|
176
|
+
return res
|
177
|
+
}
|
178
|
+
throw new Error(`response not json: ${res.headers.get('content-type')}`)
|
179
|
+
}
|
180
|
+
const err = new Error(`response ${res.statusText}`) as HttpError
|
181
|
+
err.code = res.status
|
182
|
+
if (res.status === 429) {
|
183
|
+
err.retryAfter = res.headers.get('Retry-After')
|
184
|
+
}
|
185
|
+
throw err
|
186
|
+
}
|
187
|
+
|
188
|
+
async listCategories(): Promise<FreshdeskCategory[]> {
|
189
|
+
const res = await fetch(`${this.baseUrl}/api/v2/solutions/categories`, { headers: this.defaultHeaders })
|
190
|
+
this.checkStatus(res)
|
191
|
+
return (await res.json()) as FreshdeskCategory[]
|
192
|
+
}
|
193
|
+
|
194
|
+
async listFolders(category: FreshdeskCategory): Promise<FreshdeskFolder[]> {
|
195
|
+
const res = await fetch(`${this.baseUrl}/api/v2/solutions/categories/${category.id}/folders`, {
|
196
|
+
headers: this.defaultHeaders,
|
197
|
+
})
|
198
|
+
this.checkStatus(res)
|
199
|
+
return (await res.json()) as FreshdeskFolder[]
|
200
|
+
}
|
201
|
+
|
202
|
+
async listArticles(folder: FreshdeskFolder) {
|
203
|
+
const res = await fetch(`${this.baseUrl}/api/v2/solutions/folders/${folder.id}/articles`, {
|
204
|
+
headers: this.defaultHeaders,
|
205
|
+
})
|
206
|
+
this.checkStatus(res)
|
207
|
+
return (await res.json()) as FreshdeskArticle[]
|
208
|
+
}
|
209
|
+
|
210
|
+
async updateCategoryTranslation(
|
211
|
+
id: FreshdeskCategory['id'],
|
212
|
+
locale: string,
|
213
|
+
body: FreshdeskCategoryCreate,
|
214
|
+
): Promise<FreshdeskCategory | -1> {
|
215
|
+
if (this.rateLimited) {
|
216
|
+
process.stdout.write(`Rate limited, skipping id: ${id} for ${locale}\n`)
|
217
|
+
return -1
|
218
|
+
}
|
219
|
+
try {
|
220
|
+
const res = await fetch(`${this.baseUrl}/api/v2/solutions/categories/${id}/${locale}`, {
|
221
|
+
method: 'put',
|
222
|
+
body: JSON.stringify(body),
|
223
|
+
headers: this.defaultHeaders,
|
224
|
+
})
|
225
|
+
this.checkStatus(res)
|
226
|
+
return (await res.json()) as FreshdeskCategory
|
227
|
+
} catch (err) {
|
228
|
+
const httpError = err as HttpError
|
229
|
+
if (httpError.code === 404) {
|
230
|
+
// not found, try create instead
|
231
|
+
const res2 = await fetch(`${this.baseUrl}/api/v2/solutions/categories/${id}/${locale}`, {
|
232
|
+
method: 'post',
|
233
|
+
body: JSON.stringify(body),
|
234
|
+
headers: this.defaultHeaders,
|
235
|
+
})
|
236
|
+
this.checkStatus(res2)
|
237
|
+
return (await res2.json()) as FreshdeskCategory
|
238
|
+
}
|
239
|
+
if (httpError.code === 429) {
|
240
|
+
this.rateLimited = true
|
241
|
+
}
|
242
|
+
process.stdout.write(`Error processing id ${id} for locale ${locale}: ${httpError.message}\n`)
|
243
|
+
throw err
|
244
|
+
}
|
245
|
+
}
|
246
|
+
|
247
|
+
async updateFolderTranslation(
|
248
|
+
id: FreshdeskFolder['id'],
|
249
|
+
locale: string,
|
250
|
+
body: FreshdeskFolderCreate,
|
251
|
+
): Promise<FreshdeskFolder | -1> {
|
252
|
+
if (this.rateLimited) {
|
253
|
+
process.stdout.write(`Rate limited, skipping id: ${id} for ${locale}\n`)
|
254
|
+
return -1
|
255
|
+
}
|
256
|
+
try {
|
257
|
+
const res = await fetch(`${this.baseUrl}/api/v2/solutions/folders/${id}/${locale}`, {
|
258
|
+
method: 'put',
|
259
|
+
body: JSON.stringify(body),
|
260
|
+
headers: this.defaultHeaders,
|
261
|
+
})
|
262
|
+
this.checkStatus(res)
|
263
|
+
return (await res.json()) as FreshdeskFolder
|
264
|
+
} catch (err) {
|
265
|
+
const httpError = err as HttpError
|
266
|
+
if (httpError.code === 404) {
|
267
|
+
// not found, try create instead
|
268
|
+
const res2 = await fetch(`${this.baseUrl}/api/v2/solutions/folders/${id}/${locale}`, {
|
269
|
+
method: 'post',
|
270
|
+
body: JSON.stringify(body),
|
271
|
+
headers: this.defaultHeaders,
|
272
|
+
})
|
273
|
+
this.checkStatus(res2)
|
274
|
+
return (await res2.json()) as FreshdeskFolder
|
275
|
+
}
|
276
|
+
if (httpError.code === 429) {
|
277
|
+
this.rateLimited = true
|
278
|
+
}
|
279
|
+
process.stdout.write(`Error processing id ${id} for locale ${locale}: ${httpError.message}\n`)
|
280
|
+
throw err
|
281
|
+
}
|
282
|
+
}
|
283
|
+
|
284
|
+
async updateArticleTranslation(
|
285
|
+
id: FreshdeskArticle['id'],
|
286
|
+
locale: string,
|
287
|
+
body: FreshdeskArticleCreate,
|
288
|
+
): Promise<FreshdeskArticle | -1> {
|
289
|
+
if (this.rateLimited) {
|
290
|
+
process.stdout.write(`Rate limited, skipping id: ${id} for ${locale}\n`)
|
291
|
+
return -1
|
292
|
+
}
|
293
|
+
try {
|
294
|
+
const res = await fetch(`${this.baseUrl}/api/v2/solutions/articles/${id}/${locale}`, {
|
295
|
+
method: 'put',
|
296
|
+
body: JSON.stringify(body),
|
297
|
+
headers: this.defaultHeaders,
|
298
|
+
})
|
299
|
+
this.checkStatus(res)
|
300
|
+
return (await res.json()) as FreshdeskArticle
|
301
|
+
} catch (err) {
|
302
|
+
const httpError = err as HttpError
|
303
|
+
if (httpError.code === 404) {
|
304
|
+
// not found, try create instead
|
305
|
+
const res2 = await fetch(`${this.baseUrl}/api/v2/solutions/articles/${id}/${locale}`, {
|
306
|
+
method: 'post',
|
307
|
+
body: JSON.stringify(body),
|
308
|
+
headers: this.defaultHeaders,
|
309
|
+
})
|
310
|
+
this.checkStatus(res2)
|
311
|
+
return (await res2.json()) as FreshdeskArticle
|
312
|
+
}
|
313
|
+
if (httpError.code === 429) {
|
314
|
+
this.rateLimited = true
|
315
|
+
}
|
316
|
+
process.stdout.write(`Error processing id ${id} for locale ${locale}: ${httpError.message}\n`)
|
317
|
+
throw err
|
318
|
+
}
|
319
|
+
}
|
320
|
+
}
|
321
|
+
|
322
|
+
export default FreshdeskApi
|