@vernikr/size-report 2.4.0 → 2.5.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/src/cli.js CHANGED
@@ -9,22 +9,19 @@ import {
9
9
  checkMode, coverageMode, dataMode, doctorMode, explainMode, hookMode, jsonMode, writeMode
10
10
  } from './modes.js';
11
11
 
12
- /* Вход инструмента: разбор строки, чтение проекта и доставка запроса режиму.
12
+ /* The tool's entry point: parse the line, read the project, hand the request to a mode.
13
13
  *
14
- * Здесь не осталось ни грамматики (`src/args.js`), ни самих режимов
15
- * (`src/modes.js`), ни закрепления настроек (`src/init.js`) только то, без чего
16
- * вход не вход: откуда берётся корень проекта, каким ключом назван файл настроек и
17
- * как отказ превращается в код выхода. Здесь же сказано вслух, когда настроек нет
18
- * и работа идёт на выведенных из проекта: это общее для всех режимов, а не их дело. Разделение не косметическое: цепочка
19
- * ветвлений «что запрошено» росла с каждым режимом и держала сложность входа, а
20
- * правила грамматики и тексты отказов проверяются своим каталогом
21
- * (`tools/refusals.js`), который считает их места в исходниках.
14
+ * Neither the grammar (`src/args.js`) nor the modes (`src/modes.js`) nor pinning the
15
+ * settings (`src/init.js`) is left hereonly what makes this an entry point at all:
16
+ * where the project root comes from, how the settings file is named, and how a refusal
17
+ * turns into an exit code. Saying out loud that there is no settings file and the settings
18
+ * were derived from the project belongs here as well: that holds for every mode, not for
19
+ * one of them.
22
20
  */
23
21
 
24
- /* Исполнитель запроса: ключ то, чем запрос назван (команда или режим), а «просто
25
- * запуск» пустая строка. Разбор уже проверил сочетания, поэтому здесь остаётся
26
- * выбор из готового списка, а не решение: у каждого названного есть свой
27
- * исполнитель, и вызывается он без пробежек по `if`. */
22
+ /* Request runners: the key is what the request is called (a command or a mode), and "a
23
+ * bare run" is the empty string. The parser has already checked the combinations, so this
24
+ * is a lookup with a runner for every name, not a decision. */
28
25
  const RUNNERS = {
29
26
  check: (c, x) => coverageMode(x.cfg, x.root, x.configFile, c.json),
30
27
  explain: (c, x) => explainMode(x.cfg, x.root, c.arg[0], c.json),
@@ -35,26 +32,26 @@ const RUNNERS = {
35
32
 
36
33
  const asked = (cmd) => (cmd.verb === null ? (cmd.mode === null ? '' : cmd.mode) : cmd.verb);
37
34
 
38
- /* Постановка хука без спроса здесь, а не в `doctor` и не в `hook-run`: первый
39
- * только докладывает, а второй зовётся уже из поставленного хука. Ставится один раз
40
- * в клоне и называется вслух, дальше молчит: отчёт обновляется после каждого
41
- * коммита без ручного шага (устройство и границы `src/hook.js`). */
35
+ /* Installing the hook without being asked belongs here, not in `doctor` (which only
36
+ * reports) and not in `hook-run` (which is called from an already installed hook). It
37
+ * installs once per clone, says so, and is silent afterwards: the report is rebuilt after
38
+ * every commit with no manual step (design and limits: `src/hook.js`). */
42
39
  function ensureHook(root, cfg) {
43
40
  const files = autoInstall(root, cfg);
44
41
  if (files === null) return;
45
- console.error('· хук поставлен: ' + files.join(', ') + ' — отчёт обновляется после каждого'
46
- + ' коммита (снять: ' + cliCommand('uninstall-hook') + ')');
42
+ console.error('· hook installed: ' + files.join(', ') + ' — the report is rebuilt after every'
43
+ + ' commit (remove it: ' + cliCommand('uninstall-hook') + ')');
47
44
  }
48
45
 
49
- /* Доставка. Диагностика и хук отвечают до чтения настроек: им нужен не весь
50
- * проект, а окружение, и отказывать им из-за настроек было бы неверно про
51
- * настройки они как раз и докладывают. */
46
+ /* Delivery. Diagnostics and the hook answer before the settings are read: they need the
47
+ * environment rather than the whole project, and refusing them over settings would be
48
+ * wrong the settings are exactly what they report about. */
52
49
  function deliver(cmd, base) {
53
50
  if (cmd.verb === 'doctor') return doctorMode(base.root, base.configFile, cmd.json);
54
51
  if (HOOK_COMMANDS.indexOf(cmd.verb) >= 0) return hookMode(cmd.verb, base.root, base.configFile);
55
52
  const ctx = { root: base.root, configFile: base.configFile, cfg: loadConfig(base.configFile, base.root) };
56
- // Примечание идёт в stderr: у `--json` и `--data` в stdout лежат данные, и
57
- // подмешивать в них рассказ о настройках значило бы ломать разбор.
53
+ // The note goes to stderr: `--json` and `--data` own stdout, and mixing a story about
54
+ // the settings into data would break parsing.
58
55
  if (ctx.cfg.derived) derivedLines(ctx.cfg).forEach((line) => console.error(line));
59
56
  ensureHook(base.root, ctx.cfg);
60
57
  return RUNNERS[asked(cmd)](cmd, ctx);
@@ -77,11 +74,11 @@ export function main() {
77
74
  console.error('✗ ' + e.message);
78
75
  return e.code;
79
76
  }
80
- // Непредвиденное дефект инструмента, а не тупик пользователя: так это и
81
- // сказано в тексте (иначе человек ищет ошибку у себя), а стек нужен целиком,
82
- // иначе такой отказ нечем разбирать.
83
- console.error('✗ внутренняя ошибка (это дефект инструмента, а не проекта —'
84
- + ' пришлите, пожалуйста, этот текст целиком):\n' + e.stack);
77
+ // Unexpected failures are a defect of the tool, not a dead end for the user, and the
78
+ // text says so otherwise the user looks for the mistake on their side. The stack is
79
+ // printed whole: nothing else can diagnose such a refusal.
80
+ console.error('✗ internal error (this is a defect of the tool, not of the project —'
81
+ + ' please send this text whole):\n' + e.stack);
85
82
  return EXIT.INTERNAL;
86
83
  }
87
84
  }
package/src/config.js CHANGED
@@ -9,36 +9,35 @@ import { TOKEN_DEFAULTS, TOKEN_FAMILIES } from './tokens.js';
9
9
  import { CATEGORY_ORDER } from './data.js';
10
10
  import { projectConfig } from './project.js';
11
11
 
12
- /* Настройки проекта-потребителя: значения по умолчанию, чтение и проверка.
13
- * Настройки описывают проект, а не механику, поэтому проверка стоит здесь же и
14
- * без неё не идёт ни один режим. */
12
+ /* The settings of a consumer project: defaults, reading and checking. Settings describe a project
13
+ * rather than the mechanics, which is why the check lives right here, and no mode runs without it. */
15
14
 
16
15
  export const CONFIG_NAME = 'size-table.config.json';
17
16
 
18
17
  export const DEFAULT_CONFIG = {
19
18
  output: 'size-report.html',
20
- locale: 'ru',
21
- title: '', // по умолчанию заголовок из локали
19
+ locale: 'en',
20
+ title: '', // by default: the heading from the locale
22
21
  heading: '',
23
- // Починка зов, который не может уйти в реестр: путь внутри проекта. Имя
24
- // пакета здесь не годится (`npx <имя>` в проекте без пакета — чужой код).
22
+ // The fix is a call that stays inside the project: the package name would send the reader to the registry,
23
+ // which serves a revision the project never pinned.
25
24
  fixCommand: invocation() + ' --write',
26
25
  metrics: ['raw', 'min'],
27
26
  columns: [],
28
- // `engine` чем считается метрика `min`: снятием балласта (умолчание, под ним
29
- // сняты замороженные эталоны) или настоящим сжатием минификатором.
27
+ // `engine` says what counts the `min` metric: stripping ballast (the default, and the one the
28
+ // frozen fixtures were taken under) or real compression by the minifier.
30
29
  minify: { engine: 'strip', ext: {}, guard: ['.js', '.mjs', '.cjs'] },
31
- // Токены: каким словарём считать. Семейство про модели, кодировка про число.
30
+ // Tokens: which dictionary counts them. The family is about models, the encoding about the number.
32
31
  tokens: Object.assign({}, TOKEN_DEFAULTS),
33
- // Автоматика хука: хук обновляет отчёт после каждого коммита и ставится сам
34
- // после установки пакета (`bin/postinstall.js`) и при первом запуске в проекте
35
- // (`src/hook.js`); этот ключ её выключатель (`.size-report/…` не нужен: снятие
36
- // хука возвращает проект к прежнему поведению).
32
+ // Hook automation: the hook rebuilds the report after every commit and installs itself after the
33
+ // package is installed (`bin/postinstall.js`) and on the first run in a project (`src/hook.js`);
34
+ // this key is its switch (no `.size-report/…` state is needed: removing the hook returns the
35
+ // project to its previous behaviour).
37
36
  hooks: { enabled: true },
38
37
  journal: null,
39
38
  links: { commitUrl: '' },
40
- // Слияние обычный коммит: у него есть правки разрешения конфликта, и без
41
- // строки они не попали бы в сумму дельт над текущим размером.
39
+ // A merge is an ordinary commit: it carries the edits that resolved a conflict, and without a row
40
+ // they would never reach the sum of deltas above the current size.
42
41
  rows: { merges: true, sha: true },
43
42
  skip: []
44
43
  };
@@ -56,49 +55,46 @@ export function gitRoot() {
56
55
  encoding: 'utf8', maxBuffer: MAX_BUF, env: gitEnv()
57
56
  }).trim();
58
57
  } catch (e) {
59
- // Два тупика с разной починкой«git не запустился» и «репозитория здесь нет»,
60
- // и расходятся они по тому, что сказал сам git, а не по догадке: ENOENT
61
- // значит, что не нашлась программа. Один текст на оба случая («не git-репозиторий
62
- // или git недоступен») не называл ни одного из них.
58
+ // Two dead ends with different fixes "git did not start" and "there is no repository here" —
59
+ // are told apart by what git itself said rather than by a guess: ENOENT means the program was not
60
+ // found. One text for both ("not a git repository, or git is unavailable") named neither of them.
63
61
  if (e.code === 'ENOENT') {
64
- refuseCause('нет git', 'git не запустился: его нет в PATH (таблица собирается по его'
65
- + ' истории, а смотрю я в ' + process.cwd() + ').\n'
66
- + ' починка: поставьте git (https://git-scm.com) и повторите команду');
62
+ refuseCause('git missing', 'git did not start: it is not in PATH (the table is built from its'
63
+ + ' history, and the directory I look in is ' + process.cwd() + ').\n'
64
+ + ' fix: install git (https://git-scm.com) and run the command again');
67
65
  }
68
- refuseCause('не git-репозиторий', 'git не видит здесь репозитория: таблица собирается по его'
69
- + ' истории (сейчас смотрю в ' + process.cwd() + ').\n'
70
- + ' смотрите: запущена ли команда из каталога проекта\n'
71
- + ' починка: если истории ещё нет создайте её: git init');
66
+ refuseCause('not a git repository', 'git sees no repository here: the table is built from its'
67
+ + ' history (the directory I look in is ' + process.cwd() + ').\n'
68
+ + ' see: whether the command was run from the directory of the project\n'
69
+ + ' fix: if there is no history yet, create it: git init');
72
70
  }
73
71
  }
74
72
 
75
- /* Настроек нетих выводит сам проект (`src/project.js`), и работа начинается сразу:
76
- * заводить файл ради первого запуска незачем, а `--init` закрепляет выведенное
77
- * файлом, когда его хотят править. Так бывает только с умолчательным именем: файл,
78
- * названный ключом `--config`, это уже запрос про конкретный файл, и его
79
- * отсутствие остаётся отказом (иначе опечатка в пути молча дала бы чужие настройки).
80
- * `path` назван словами, а не путём: файла нет, и текст «правьте <путь>» привёл бы
81
- * человека к тому, чего в проекте не лежит. */
73
+ /* No settings file the project derives them (`src/project.js`) and work starts at once: there is no
74
+ * reason to create a file for a first run, and `--init` pins the derived ones to a file when someone
75
+ * wants to edit them. This happens for the default name only: a file named by `--config` is already a
76
+ * request for that very file, so its absence stays a refusal (otherwise a typo in the path would
77
+ * silently yield someone else's settings). `path` is given in words rather than as a path: there is no
78
+ * file, and "edit <path>" would lead the reader to something the project does not have. */
82
79
  export function derivedConfig(root) {
83
80
  const cfg = derivedProfile(root);
84
- cfg.path = 'настройки, выведенные из проекта';
81
+ cfg.path = 'derived from the project';
85
82
  cfg.derived = true;
86
83
  validateConfig(cfg);
87
84
  return cfg;
88
85
  }
89
86
 
90
- /* Выведенное из проекта + умолчаниято, чем проект работает без файла, и то,
91
- * что закрепляет `--init`. Одно место на две роли (иначе «файл» и «работа без
92
- * файла» разошлись бы колонкой или числом), а сам вывод (`src/project.js`) о
93
- * умолчаниях не знает: он говорит только то, что видит в проекте. */
87
+ /* Derived from the project plus the defaults what a project runs on without a file, and what
88
+ * `--init` pins. One place for two roles (or "the file" and "work without a file" would drift by a
89
+ * column or a number), while the deriving itself (`src/project.js`) knows nothing of the defaults: it
90
+ * says only what it sees in the project. */
94
91
  export function derivedProfile(root) {
95
92
  return withDefaults(projectConfig(root));
96
93
  }
97
94
 
98
- /* Настройки поверх умолчанийодним местом на два источника (файл и проект):
99
- * вложенное досыпается по ключам, потому что `minify: {engine: …}` не значит «у
100
- * `minify` больше нет других полей», а значило бы, что снятие балласта потеряло
101
- * список расширений. */
95
+ /* Settings on top of the defaults one place for two sources (a file and the project): nested keys
96
+ * are filled in by key, because `minify: {engine: …}` does not mean "`minify` has no other fields",
97
+ * and reading it that way would lose the stripping's list of extensions. */
102
98
  function withDefaults(raw) {
103
99
  const cfg = Object.assign({}, DEFAULT_CONFIG, raw);
104
100
  ['minify', 'tokens', 'hooks', 'links', 'rows'].forEach((key) => {
@@ -107,25 +103,25 @@ function withDefaults(raw) {
107
103
  return cfg;
108
104
  }
109
105
 
110
- /* Настройки читаются как есть и досыпаются значениями по умолчанию: у проекта,
111
- * который только подключил генератор, конфиг может быть в три строки. */
106
+ /* Settings are read as they are and filled in with the defaults: in a project that has just attached
107
+ * the generator the config may be three lines long. */
112
108
  export function loadConfig(file, root) {
113
109
  if (!fs.existsSync(file)) {
114
- // Совет называет тот же файл, о котором шла речь: `--init` без файла записал бы
115
- // черновик под умолчательным именем в корне проекта то есть починил бы не то,
116
- // о чём спросили. Имя не называем ровно тогда, когда оно и так умолчательное.
110
+ // The advice names the very file in question: `--init` without a file would write a draft under the
111
+ // default name in the project rootfixing something other than what was asked. The name is left
112
+ // out exactly when it is the default one anyway.
117
113
  const dflt = root !== undefined && path.resolve(root, CONFIG_NAME) === path.resolve(file);
118
114
  if (dflt) return derivedConfig(root);
119
- refuseCause('нет файла настроек', 'нет файла настроек ' + file
120
- + '\n создайте его: ' + cliCommand('--init ' + advicePath(file))
121
- + '\n смотрите: без «--config» настройки не нужныони выводятся из проекта');
115
+ refuseCause('no settings file', 'no settings file ' + file
116
+ + '\n create it: ' + cliCommand('--init ' + advicePath(file))
117
+ + '\n see: without "--config" no settings are needed they are derived from the project');
122
118
  }
123
119
  let raw;
124
120
  try {
125
121
  raw = JSON.parse(fs.readFileSync(file, 'utf8'));
126
122
  } catch (e) {
127
- refuseCause('настройки не разобраны', 'не разобран ' + file + ': ' + e.message
128
- + '\n починка: правьте ' + file + '; образец настроек даёт ' + cliCommand('--init') + ' в пустом каталоге');
123
+ refuseCause('settings not parsed', 'cannot parse ' + file + ': ' + e.message
124
+ + '\n fix: edit ' + file + '; a sample of settings comes from ' + cliCommand('--init') + ' in an empty directory');
129
125
  }
130
126
  const cfg = withDefaults(raw);
131
127
  cfg.path = file;
@@ -133,23 +129,23 @@ export function loadConfig(file, root) {
133
129
  return cfg;
134
130
  }
135
131
 
136
- /* Колонки: метка и пути имена, а не что попало: путь числом или объектом молча не
137
- * совпадает ни с чем, и колонка отчитывается нулём строк за успех. Отказ обязан
138
- * случиться здесь, а не превратиться в пустой отчёт. */
132
+ /* Columns: a label and paths have to be names rather than anything at all a path given as a number
133
+ * or an object silently matches nothing, and the column reports zero rows as success. The refusal has
134
+ * to happen here instead of turning into an empty report. */
139
135
  function checkColumns(cfg, fail) {
140
- if (!cfg.columns || cfg.columns.length === 0) fail('не задано ни одной колонки (columns)');
136
+ if (!cfg.columns || cfg.columns.length === 0) fail('no columns are given (columns)');
141
137
  const labels = new Set();
142
138
  cfg.columns.forEach((c, i) => {
143
139
  const pathsAreNames = c && Array.isArray(c.paths)
144
140
  && c.paths.length > 0 && c.paths.every((p) => typeof p === 'string' && p !== '');
145
141
  if (!c || typeof c.label !== 'string' || c.label === '' || !pathsAreNames) {
146
- fail('колонка ' + (i + 1) + ' должна быть {label, paths: [...]} из непустых строк: '
147
- + JSON.stringify(c).slice(0, 90) + '\n смотрите: черновик с готовыми колонками даёт '
148
- + cliCommand('--init <файл>'));
142
+ fail('column #' + (i + 1) + ' has to be {label, paths: [...]} of non-empty strings: '
143
+ + JSON.stringify(c).slice(0, 90) + '\n see: a draft with ready columns comes from '
144
+ + cliCommand('--init <file>'));
149
145
  }
150
- if (labels.has(c.label)) fail('метка колонки «' + c.label + '» повторяется');
146
+ if (labels.has(c.label)) fail('the column label "' + c.label + '" repeats');
151
147
  if (c.category !== undefined && CATEGORY_ORDER.indexOf(c.category) < 0) {
152
- fail('категория «' + c.category + '» у колонки «' + c.label + '» неизвестна: '
148
+ fail('the category "' + c.category + '" of the column "' + c.label + '" is unknown: '
153
149
  + CATEGORY_ORDER.join(', '));
154
150
  }
155
151
  labels.add(c.label);
@@ -157,51 +153,51 @@ function checkColumns(cfg, fail) {
157
153
  }
158
154
 
159
155
  function checkMetrics(cfg, fail) {
160
- if (!Array.isArray(cfg.metrics) || cfg.metrics.length === 0) fail('не заданы метрики (metrics)');
156
+ if (!Array.isArray(cfg.metrics) || cfg.metrics.length === 0) fail('no metrics are given (metrics)');
161
157
  cfg.metrics.forEach((m) => {
162
- if (!METRICS[m]) fail('неизвестная метрика «' + m + '» (есть: ' + Object.keys(METRICS).join(', ') + ')');
158
+ if (!METRICS[m]) fail('unknown metric "' + m + '" (there are: ' + Object.keys(METRICS).join(', ') + ')');
163
159
  });
164
160
  }
165
161
 
166
162
  function checkMinify(cfg, fail) {
167
163
  if (MINIFY_ENGINES.indexOf(cfg.minify.engine) < 0) {
168
- fail('неизвестный способ минификации «' + cfg.minify.engine + '» (есть: ' + MINIFY_ENGINES.join(', ') + ')');
164
+ fail('unknown minification engine "' + cfg.minify.engine + '" (there are: ' + MINIFY_ENGINES.join(', ') + ')');
169
165
  }
170
166
  }
171
167
 
172
168
  function checkTokens(cfg, fail) {
173
169
  const family = TOKEN_FAMILIES[cfg.tokens.family];
174
170
  if (family === undefined) {
175
- fail('неизвестное семейство токенизатора «' + cfg.tokens.family + '» (есть: '
171
+ fail('unknown tokenizer family "' + cfg.tokens.family + '" (there are: '
176
172
  + Object.keys(TOKEN_FAMILIES).join(', ') + ')');
177
173
  }
178
174
  if (family.encodings.indexOf(cfg.tokens.encoding) < 0) {
179
- fail('неизвестная кодировка токенизатора «' + cfg.tokens.encoding + '» у семейства '
180
- + cfg.tokens.family + ' (есть: ' + family.encodings.join(', ') + ')');
175
+ fail('unknown tokenizer encoding "' + cfg.tokens.encoding + '" of the family '
176
+ + cfg.tokens.family + ' (there are: ' + family.encodings.join(', ') + ')');
181
177
  }
182
178
  }
183
179
 
184
- /* Файл таблицы не может быть её колонкой: размер артефакта зависит от числа строк,
185
- * то есть от самого себя. */
180
+ /* The report file cannot be a column of itself: the size of the artifact depends on the number of
181
+ * rows, that is, on itself. */
186
182
  function checkOutput(cfg, fail) {
187
183
  cfg.columns.forEach((c) => {
188
- if (c.paths.indexOf(cfg.output) >= 0) fail('файл таблицы (' + cfg.output + ') не может быть колонкой');
184
+ if (c.paths.indexOf(cfg.output) >= 0) fail('the size table file (' + cfg.output + ') cannot be a column');
189
185
  });
190
- if (!cfg.output) fail('не задан output');
186
+ if (!cfg.output) fail('output is not set');
191
187
  }
192
188
 
193
189
  function checkJournal(cfg, fail) {
194
190
  if (!cfg.journal) return;
195
- if (!cfg.journal.path) fail('journal.path не задан');
196
- if (!cfg.journal.pattern) fail('journal.pattern не задан');
197
- try { new RegExp(cfg.journal.pattern); } catch (e) { fail('journal.pattern не компилируется: ' + e.message); }
191
+ if (!cfg.journal.path) fail('journal.path is not set');
192
+ if (!cfg.journal.pattern) fail('journal.pattern is not set');
193
+ try { new RegExp(cfg.journal.pattern); } catch (e) { fail('journal.pattern does not compile: ' + e.message); }
198
194
  }
199
195
 
200
- /* Что настройки говорят про путь: `columns` — его отслеживает колонка, `excluded` — он
201
- * объявлен исключением (`skip` и сам файл отчёта), `outside` — мимо того и другого.
202
- * Суждение одно на два ответа: `check` спрашивает его про всю историю, `explain` про
203
- * один коммит. Знакомство считается по колонкам целиком, а не по метке: у колонки путей
204
- * может быть несколько (переименование), и любой из них — она сама. */
196
+ /* What the settings say about a path: `columns` — a column tracks it, `excluded` — it is declared an
197
+ * exception (`skip` and the report file itself), `outside` — neither. One judgement for two answers:
198
+ * `check` asks it about the whole history, `explain` about a single commit. Membership is counted over
199
+ * all paths of a column rather than by its label: a column may have several paths (a rename), and any
200
+ * of them is that column. */
205
201
  export function pathRoles(cfg) {
206
202
  const tracked = new Set();
207
203
  cfg.columns.forEach((col) => col.paths.forEach((p) => tracked.add(p)));
@@ -212,26 +208,26 @@ export function pathRoles(cfg) {
212
208
  };
213
209
  }
214
210
 
215
- /* Починка «мимо колонок»один текст на два ответа, и он называет пути: команда без
216
- * имён не команда. Текст собирается из имён, а не приписывает их по ветке, поэтому и
217
- * предусматривать тут нечего: коммит без файлов вовсе починки не получаетна пустом
218
- * списке её не зовут (см. `fixFor` в `src/explain.js`). */
211
+ /* The fix for "outside the columns" one text for two answers, and it names the paths: a command
212
+ * without names is no command. The text is assembled from the names rather than appending them per
213
+ * branch, which is why nothing has to be guarded here: a commit with no files at all gets no fix the
214
+ * list being empty, nobody calls it (`fixFor` in `src/explain.js`). */
219
215
  export function outsideFix(paths) {
220
- return 'допишите эти пути колонкой или в «skip» файла ' + CONFIG_NAME + ': ' + paths.join(', ');
216
+ return 'add these paths as a column or to "skip" of ' + CONFIG_NAME + ': ' + paths.join(', ');
221
217
  }
222
218
 
223
219
  export function validateConfig(cfg) {
224
- const fail = (msg) => refuseCause('настройки неверны',
225
- 'конфиг ' + cfg.path + ': ' + msg + '\n починка: правьте ' + cfg.path);
220
+ const fail = (msg) => refuseCause('settings invalid',
221
+ 'config ' + cfg.path + ': ' + msg + '\n fix: edit ' + cfg.path);
226
222
  checkColumns(cfg, fail);
227
223
  checkMetrics(cfg, fail);
228
224
  checkMinify(cfg, fail);
229
225
  checkTokens(cfg, fail);
230
- if (!LOCALES[cfg.locale]) fail('неизвестная локаль «' + cfg.locale + '» (есть: ' + Object.keys(LOCALES).join(', ') + ')');
231
- // Выключатель хука «да/нет», а не «правда/ложь»: `false` от случайной строки
232
- // отличать обязан инструмент, иначе выключенная автоматика осталась бы включённой.
226
+ if (!LOCALES[cfg.locale]) fail('unknown locale "' + cfg.locale + '" (there are: ' + Object.keys(LOCALES).join(', ') + ')');
227
+ // The hook switch is a yes/no rather than a truthy/falsy one: the tool has to tell `false` from a
228
+ // stray string, or switched-off automation would stay switched on.
233
229
  if (typeof cfg.hooks.enabled !== 'boolean') {
234
- fail('hooks.enabled не «да/нет»: ' + JSON.stringify(cfg.hooks.enabled));
230
+ fail('hooks.enabled is not a yes/no: ' + JSON.stringify(cfg.hooks.enabled));
235
231
  }
236
232
  checkOutput(cfg, fail);
237
233
  checkJournal(cfg, fail);
package/src/css.js CHANGED
@@ -1,23 +1,23 @@
1
1
  import fs from 'node:fs';
2
2
 
3
- /* Оформление отчёта обычные `.css` рядом с кодом, а не строки внутри модулей:
4
- * то же правило, что и у программы страницы (`src/page/app.js`), исходник видит
5
- * редактор, а не только шаблонная строка. Читаются они с диска относительно
6
- * своего места, поэтому работают и у того, кто поставил пакет.
3
+ /* The report's styling is ordinary `.css` next to the code rather than strings inside modules — the same rule
4
+ * as for the page's program (`src/page/app.js`): an editor sees a real source file instead of a template
5
+ * string. They are read from disk relative to their own place, so they work for whoever installed the package
6
+ * as well.
7
7
  *
8
- * Наборов два, и у каждого своя роль:
8
+ * There are two sets, each with a role of its own:
9
9
  *
10
- * 1. `table.css` — **таблица**: геометрия клеток, липкие шапка и колонка коммита,
11
- * подпись коммита, цвета дельт.
12
- * 2. `page/app.css` — оформление страницы **сверх таблицы**: холст, панель
13
- * выбора, состояния пустоты и адаптации под узкое окно.
10
+ * 1. `table.css` — the **table**: cell geometry, the sticky header and commit column, a commit's caption,
11
+ * the colours of the deltas.
12
+ * 2. `page/app.css` — the page's look **on top of the table**: the canvas, the panel of choices, the empty
13
+ * states and the adaptation to a narrow window.
14
14
  *
15
- * Соглашение о цвете дельт задано один раз в `table.css`: `.up` зелёный, `.down`
16
- * красный (рост «больше логики», а не тревога). Второго места у него нет
17
- * намеренно: рост не может быть показан разными цветами в двух местах одной
18
- * страницы. Смена соглашения — две строки в `table.css`.
15
+ * The convention about the colour of a delta is set once, in `table.css`: `.up` green, `.down` red (growth is
16
+ * "more logic" rather than alarm). It has no second place on purpose: growth cannot be shown in different
17
+ * colours in two spots of one page. Changing the convention is two lines in `table.css`.
19
18
  *
20
- * Путь у `readCss` от каталога `src/`: так его видит движок, где бы он ни лежал.
19
+ * The path given to `readCss` is relative to the `src/` directory: that is how the engine sees it wherever it
20
+ * lies.
21
21
  */
22
22
 
23
23
  export const TABLE_CSS = readCss('./table.css');