gulp-mu-gulp-api 0.2.1 → 0.3.1

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/README.md CHANGED
@@ -20,10 +20,19 @@ The module is deliberately **dependency-free** and knows nothing about µGulp™
20
20
  npm install gulp-mu-gulp-api
21
21
  ```
22
22
 
23
+ ### 0.3.0
24
+
25
+ - **Added** integrated i18x console layer: `Log`, `Warn`, `LogError`, `Translate`, `GetLid`, `SetLid`, `GetTimeZone`, `DateInTimeZone`, `InstallStringExtensions` — translations live in the consumer project's `i18x/gulp/<lid>.json`; language follows `MICROGULP_LANG` (set by the µGulp dashboard) with locale fallback.
26
+ - **Added** µLib-compatible `String.prototype.i18xTrans()` / `.i18xRegister()` (installed automatically on import).
27
+ - **Removed** `µMeta()` / `MicroGulpMeta` — embed the context tag in the source phrase and use `.i18xRegister()` instead (see below).
28
+
23
29
  ## Usage
24
30
 
25
31
  ```javascript
26
- import { ReportProgress, CreateProgress, PlaySignal, Speak, RequestTextInput, RequestColorInput, RequestFontInput, RequestSelectInput, RequestMultiSelectInput, RequestForm, IsMicroGulp, IsµGulp, µMeta } from 'gulp-mu-gulp-api';
32
+ import { ReportProgress, CreateProgress, PlaySignal, Speak, RequestTextInput, RequestColorInput, RequestFontInput, RequestSelectInput, RequestMultiSelectInput, RequestForm, IsMicroGulp, IsµGulp, InstallStringExtensions } from 'gulp-mu-gulp-api';
33
+
34
+ // Enables "phrase<context=\"…\"/>".i18xRegister()/.i18xTrans() on strings.
35
+ InstallStringExtensions();
27
36
 
28
37
  export async function BUILD_THEME() {
29
38
  // single inputs
@@ -50,8 +59,8 @@ export async function BUILD_THEME() {
50
59
  PlaySignal('success');
51
60
  Speak('Theme build finished.');
52
61
  }
53
- BUILD_THEME.µDisplayName = µMeta('Build Theme', 'µDisplayName');
54
- BUILD_THEME.µDescription = µMeta('Compiles the Carbon/Neon dashboard skin.', 'µDescription');
62
+ BUILD_THEME.µDisplayName = 'Build Theme<context="µDisplayName"/>'.i18xRegister();
63
+ BUILD_THEME.µDescription = 'Compiles the Carbon/Neon dashboard skin.<context="µDescription"/>'.i18xRegister();
55
64
  ```
56
65
 
57
66
  Complete forms in one go:
@@ -75,7 +84,10 @@ let values = await RequestForm({
75
84
  | Function | Purpose |
76
85
  | :--- | :--- |
77
86
  | `IsMicroGulp()` / `IsµGulp()` | `true` when the task runs under µGulp™ with an attached dashboard (`IsUGulp` is a deprecated alias) |
78
- | `µMeta(text, property)` | Marks a µXXX task metadata string with an i18xe context tag (`µDisplayName`, `µDescription`, `µTooltip`, `µGroup`) |
87
+ | `InstallStringExtensions()` | Installs µLib-compatible `String.prototype.i18xTrans()`/`i18xRegister()` (auto-run on import) |
88
+ | `Log(text, values?)` / `Warn` / `LogError` | Localized `console.*` via i18x (`<context="task log"/>` etc.) |
89
+ | `Translate(text, values?)` | Returns the translated string without logging |
90
+ | `GetLid()` / `SetLid(lid?)` | Active language id; `SetLid` clears to automatic when omitted |
79
91
  | `ReportProgress(value, label?)` | Report progress `0..1` to the task progress bar |
80
92
  | `CreateProgress(label?)` | Reporter object with `Update(value, stepLabel?)` and `Done()` |
81
93
  | `PlaySignal(signal?)` | Acoustic signal in the dashboard: `'success'` \| `'error'` \| `'attention'` (default) |
@@ -87,6 +99,29 @@ let values = await RequestForm({
87
99
  | `RequestSelectInput(options)` | Single selection from a fixed option list |
88
100
  | `RequestMultiSelectInput(options)` | Multi selection (checkbox group), always resolves to a `string[]` |
89
101
 
102
+ ### Localized task metadata (`.i18xRegister()` + placeholders)
103
+
104
+ Write the i18xe context tag **into** the source phrase and mark it with `.i18xRegister()` — that marker is what the `i18xe-sync` tooling scans for, and at runtime it returns the phrase unchanged (the classic gulp CLI keeps seeing a plain string). Use i18xe placeholder tags in the source text. The dashboard substitutes a standard property set when translating:
105
+
106
+ | Placeholder | Source |
107
+ | :--- | :--- |
108
+ | `version` | `µI18nContext.version` export, else `package.json` |
109
+ | `project` | `µI18nContext.project` export, else `package.json` `name` |
110
+ | `workspace` | workspace folder name |
111
+ | `source` | gulpfile path relative to the workspace |
112
+ | `gulpfile` | gulpfile file name |
113
+ | `task` | technical export name |
114
+ | `taskId` | composite id `source::task` |
115
+
116
+ Export defaults from the gulpfile (overrides `package.json`):
117
+
118
+ ```javascript
119
+ export const µI18nContext = { version: '1.13', project: 'eMP' };
120
+
121
+ MAKE_BUILDS.µDisplayName = 'Make Build V<version/><context="µDisplayName"/>'.i18xRegister();
122
+ // => "Make Build V1.13" in the dashboard (translatable per language)
123
+ ```
124
+
90
125
  ### Field types (for `RequestForm`)
91
126
 
92
127
  A field descriptor is `{ id, type, label, default?, options?, validate?, min?, max?, step?, rows?, placeholder? }`.
@@ -143,10 +178,19 @@ Das Modul ist bewusst **abhängigkeitsfrei** und kennt keine µGulp™-Interna:
143
178
  npm install gulp-mu-gulp-api
144
179
  ```
145
180
 
181
+ ### 0.3.0
182
+
183
+ - **Neu** integrierte i18x-Konsolenschicht: `Log`, `Warn`, `LogError`, `Translate`, `GetLid`, `SetLid`, `GetTimeZone`, `DateInTimeZone`, `InstallStringExtensions` — Übersetzungen liegen im Verbraucherprojekt unter `i18x/gulp/<lid>.json`; die Sprache folgt `MICROGULP_LANG` (vom µGulp-Dashboard gesetzt) mit Locale-Fallback.
184
+ - **Neu** µLib-kompatible `String.prototype.i18xTrans()` / `.i18xRegister()` (werden beim Import automatisch installiert).
185
+ - **Entfernt** `µMeta()` / `MicroGulpMeta` — Kontext-Tag in die Textphrase schreiben und `.i18xRegister()` verwenden (siehe unten).
186
+
146
187
  ## Verwendung
147
188
 
148
189
  ```javascript
149
- import { ReportProgress, CreateProgress, PlaySignal, Speak, RequestTextInput, RequestColorInput, RequestFontInput, RequestSelectInput, RequestMultiSelectInput, RequestForm, IsMicroGulp, IsµGulp, µMeta } from 'gulp-mu-gulp-api';
190
+ import { ReportProgress, CreateProgress, PlaySignal, Speak, RequestTextInput, RequestColorInput, RequestFontInput, RequestSelectInput, RequestMultiSelectInput, RequestForm, IsMicroGulp, IsµGulp, InstallStringExtensions } from 'gulp-mu-gulp-api';
191
+
192
+ // Enables "phrase<context=\"…\"/>".i18xRegister()/.i18xTrans() on strings.
193
+ InstallStringExtensions();
150
194
 
151
195
  export async function BUILD_THEME() {
152
196
  // Einzeleingaben
@@ -173,8 +217,8 @@ export async function BUILD_THEME() {
173
217
  PlaySignal('success');
174
218
  Speak('Theme build finished.');
175
219
  }
176
- BUILD_THEME.µDisplayName = µMeta('Build Theme', 'µDisplayName');
177
- BUILD_THEME.µDescription = µMeta('Kompiliert den Carbon/Neon-Dashboard-Skin.', 'µDescription');
220
+ BUILD_THEME.µDisplayName = 'Build Theme<context="µDisplayName"/>'.i18xRegister();
221
+ BUILD_THEME.µDescription = 'Kompiliert den Carbon/Neon-Dashboard-Skin.<context="µDescription"/>'.i18xRegister();
178
222
  ```
179
223
 
180
224
  Komplette Formulare in einem Rutsch:
@@ -198,7 +242,10 @@ let values = await RequestForm({
198
242
  | Funktion | Zweck |
199
243
  | :--- | :--- |
200
244
  | `IsMicroGulp()` / `IsµGulp()` | `true`, wenn der Task unter µGulp™ mit angebundenem Dashboard läuft (`IsUGulp` ist ein veralteter Alias) |
201
- | `µMeta(text, property)` | Kennzeichnet einen µXXX-Metadaten-String mit einem i18xe-Kontext-Tag (`µDisplayName`, `µDescription`, `µTooltip`, `µGroup`) |
245
+ | `InstallStringExtensions()` | Installiert µLib-kompatible `String.prototype.i18xTrans()`/`i18xRegister()` (läuft beim Import automatisch) |
246
+ | `Log(text, values?)` / `Warn` / `LogError` | Lokalisierte `console.*`-Ausgabe über i18x (`<context="task log"/>` usw.) |
247
+ | `Translate(text, values?)` | Gibt den übersetzten String zurück, ohne zu loggen |
248
+ | `GetLid()` / `SetLid(lid?)` | Aktive Sprach-ID; `SetLid` ohne Argument = automatische Auflösung |
202
249
  | `ReportProgress(value, label?)` | Fortschritt `0..1` an die Task-Progressbar melden |
203
250
  | `CreateProgress(label?)` | Reporter-Objekt mit `Update(value, stepLabel?)` und `Done()` |
204
251
  | `PlaySignal(signal?)` | Akustisches Signal im Dashboard: `'success'` \| `'error'` \| `'attention'` (Default) |
@@ -210,6 +257,29 @@ let values = await RequestForm({
210
257
  | `RequestSelectInput(options)` | Einzelauswahl aus fester Optionsliste |
211
258
  | `RequestMultiSelectInput(options)` | Mehrfachauswahl (Checkbox-Gruppe), löst immer mit `string[]` auf |
212
259
 
260
+ ### Lokalisierte Task-Metadaten (`.i18xRegister()` + Platzhalter)
261
+
262
+ Den i18xe-Kontext-Tag **in** die Textphrase schreiben und mit `.i18xRegister()` markieren — genau diese Markierung scannt das `i18xe-sync`-Tooling, zur Laufzeit gibt sie den Text unverändert zurück (die klassische gulp-CLI sieht weiterhin einen einfachen String). i18xe-Platzhalter im Quelltext verwenden. Beim Übersetzen setzt das Dashboard einen Standardsatz ein:
263
+
264
+ | Platzhalter | Quelle |
265
+ | :--- | :--- |
266
+ | `version` | `µI18nContext.version`-Export, sonst `package.json` |
267
+ | `project` | `µI18nContext.project`-Export, sonst `package.json` `name` |
268
+ | `workspace` | Workspace-Ordnername |
269
+ | `source` | Gulpfile-Pfad relativ zum Workspace |
270
+ | `gulpfile` | Gulpfile-Dateiname |
271
+ | `task` | technischer Exportname |
272
+ | `taskId` | zusammengesetzte Id `source::task` |
273
+
274
+ Defaults im Gulpfile exportieren (`package.json` wird überschrieben):
275
+
276
+ ```javascript
277
+ export const µI18nContext = { version: '1.13', project: 'eMP' };
278
+
279
+ MAKE_BUILDS.µDisplayName = 'Make Build V<version/><context="µDisplayName"/>'.i18xRegister();
280
+ // => „Make Build V1.13“ im Dashboard (pro Sprache übersetzbar)
281
+ ```
282
+
213
283
  ### Feldtypen (für `RequestForm`)
214
284
 
215
285
  Ein Feld-Deskriptor ist `{ id, type, label, default?, options?, validate?, min?, max?, step?, rows?, placeholder? }`.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gulp-mu-gulp-api",
3
- "version": "0.2.1",
4
- "description": "Public task API for the µGulp orchestrator: progress reporting, sound signals, speech output and interactive UI inputs (text, password, number, textarea, color, font, select, radio, multi-select checkbox, range slider, date/time, file) from within gulp tasks — with graceful CLI fallbacks when running without µGulp.",
3
+ "version": "0.3.1",
4
+ "description": "Public task API for the µGulp orchestrator: progress reporting, sound signals, speech output, localized console output (i18x) and interactive UI inputs (text, password, number, textarea, color, font, select, radio, multi-select checkbox, range slider, date/time, file) from within gulp tasks — with graceful CLI fallbacks when running without µGulp.",
5
5
  "type": "module",
6
6
  "main": "src/index.mjs",
7
7
  "exports": {
@@ -22,6 +22,9 @@
22
22
  "interactive",
23
23
  "sound",
24
24
  "speech",
25
+ "i18n",
26
+ "i18x",
27
+ "localization",
25
28
  "prompt",
26
29
  "multiselect",
27
30
  "form",
package/src/i18x.mjs ADDED
@@ -0,0 +1,306 @@
1
+ // ===========================================
2
+ // gulp-mu-gulp-api — i18x console output for gulp tasks
3
+ // © 2026 Meinolf Amekudzi
4
+ // (published under MIT license)
5
+ // ===========================================
6
+
7
+ /**
8
+ * Localized console output for gulp tasks, following the i18xe notation:
9
+ * source strings are American English and carry a context tag
10
+ * (`<context="task log"/>` …); translations live in the *consumer project's*
11
+ * own `i18x/gulp/<lid>.json` files and `<name/>` placeholders are filled at
12
+ * render time. Any project using µGulp drops an `i18x/gulp` directory next to
13
+ * its gulpfile and its build output is localized — no coupling to µGulp
14
+ * internals, works from the plain `gulp` CLI as well.
15
+ *
16
+ * The active language resolves from `MICROGULP_LANG` (the µGulp engine
17
+ * forwards the dashboard language here), then the host OS locale, then the
18
+ * en-US source. Regional/script variants fall back through a compact map
19
+ * (`de-AT` → `de-DE`, `zh-TW` → `zh-CN`, `en-GB` → `en-US`, …) onto whichever
20
+ * languages the project actually ships.
21
+ *
22
+ * @example
23
+ * import { Log, Warn } from 'gulp-mu-gulp-api';
24
+ * export async function BUILD() {
25
+ * Log('Compiling styles\u2026<context="task log"/>');
26
+ * Log('Chunk <n/>/<total/> written<context="task log"/>', { n: 3, total: 5 });
27
+ * }
28
+ *
29
+ * @module gulp-mu-gulp-api/i18x
30
+ */
31
+
32
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
33
+ import { join } from 'node:path';
34
+
35
+ const DEFAULT_LID = 'en-US';
36
+
37
+ // Compact locale fallback tables (subset of the µLib i18x LIDS_MAPS /
38
+ // LIDS_MAIN_CULTURES). The resolver also falls back on the bare language
39
+ // prefix, so only the non-obvious chains need listing.
40
+ const LIDS_MAPS = {
41
+ 'zh-CHS': ['zh-CN'], 'zh-CHT': ['zh-CHS', 'zh-CN'], 'zh-HK': ['zh-CHS', 'zh-CN'],
42
+ 'zh-Hans': ['zh-CHS', 'zh-CN'], 'zh-Hant': ['zh-CHT', 'zh-CHS', 'zh-CN'],
43
+ 'zh-MO': ['zh-CHS', 'zh-CN'], 'zh-SG': ['zh-CHS', 'zh-CN'], 'zh-TW': ['zh-CHT', 'zh-CHS', 'zh-CN'],
44
+ 'de-CH': ['de-DE'], 'de-AT': ['de-DE'], 'de-LU': ['de-DE'], 'de-LI': ['de-DE'],
45
+ 'en-GB': ['en-US'], 'en-AU': ['en-GB', 'en-US'], 'en-CA': ['en-GB', 'en-US'],
46
+ };
47
+ const LIDS_MAIN_CULTURES = {
48
+ zh: 'CN', de: 'DE', en: 'GB', fr: 'FR', es: 'ES', it: 'IT', pt: 'PT',
49
+ nl: 'NL', ru: 'RU', ja: 'JP', ko: 'KR',
50
+ };
51
+
52
+ let dictionaryCache = new Map();
53
+ let availableLids = null;
54
+ let activeLid = null;
55
+ const timeZoneFormatters = {};
56
+
57
+ /** @returns {string} IANA timezone from MICROGULP_TZ, then ECMAScript Intl */
58
+ export function GetTimeZone() {
59
+ if (process.env.MICROGULP_TZ) return process.env.MICROGULP_TZ;
60
+ try {
61
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
62
+ } catch {
63
+ return 'UTC';
64
+ }
65
+ }
66
+
67
+ /** Wall-clock Date in an IANA timezone (µLib-compatible). */
68
+ export function DateInTimeZone(_date, _timeZone) {
69
+ let tz = _timeZone || GetTimeZone();
70
+ try {
71
+ let formatter = timeZoneFormatters[tz];
72
+ if (formatter === undefined) {
73
+ formatter = new Intl.DateTimeFormat('en-US', {
74
+ timeZone: tz, hour12: false,
75
+ year: 'numeric', month: '2-digit', day: '2-digit',
76
+ hour: '2-digit', minute: '2-digit', second: '2-digit',
77
+ });
78
+ timeZoneFormatters[tz] = formatter;
79
+ }
80
+ let values = {};
81
+ for (let part of formatter.formatToParts(_date)) values[part.type] = part.value;
82
+ return new Date(
83
+ parseInt(values.year, 10), parseInt(values.month, 10) - 1, parseInt(values.day, 10),
84
+ parseInt(values.hour, 10) % 24, parseInt(values.minute, 10), parseInt(values.second, 10),
85
+ _date.getMilliseconds(),
86
+ );
87
+ } catch {
88
+ return _date;
89
+ }
90
+ }
91
+
92
+ /** Normalizes casing: language lower, 2-letter region upper, 4-letter script Title. */
93
+ function _NormalizeLid(_lid) {
94
+ if (!_lid) return '';
95
+ let parts = String(_lid).replace(/_/g, '-').trim().split('-').filter(Boolean);
96
+ if (parts.length === 0) return '';
97
+ return parts.map((_part, _index) => {
98
+ if (_index === 0) return _part.toLowerCase();
99
+ if (_part.length === 2) return _part.toUpperCase();
100
+ if (_part.length === 4) return _part.charAt(0).toUpperCase() + _part.slice(1).toLowerCase();
101
+ return _part;
102
+ }).join('-');
103
+ }
104
+
105
+ /**
106
+ * Picks the closest shipped language for a wanted locale: exact
107
+ * (case-insensitive) → LIDS_MAPS chain → main culture → language prefix →
108
+ * en-US default → first available.
109
+ */
110
+ function _BestAvailableLid(_wantedLid, _available) {
111
+ if (_available.length === 0) return DEFAULT_LID;
112
+ let wanted = _NormalizeLid(_wantedLid);
113
+ if (!wanted) return _available.includes(DEFAULT_LID) ? DEFAULT_LID : _available[0];
114
+ if (_available.includes(wanted)) return wanted;
115
+ let caseInsensitive = _available.find((_lid) => _lid.toLowerCase() === wanted.toLowerCase());
116
+ if (caseInsensitive) return caseInsensitive;
117
+ if (Object.prototype.hasOwnProperty.call(LIDS_MAPS, wanted)) {
118
+ for (let candidate of LIDS_MAPS[wanted]) if (_available.includes(candidate)) return candidate;
119
+ }
120
+ let language = wanted.split('-')[0];
121
+ if (Object.prototype.hasOwnProperty.call(LIDS_MAIN_CULTURES, language)) {
122
+ let mainCulture = language + '-' + LIDS_MAIN_CULTURES[language];
123
+ if (_available.includes(mainCulture)) return mainCulture;
124
+ }
125
+ let byPrefix = _available.find((_lid) => _lid.split('-')[0].toLowerCase() === language);
126
+ if (byPrefix) return byPrefix;
127
+ return _available.includes(DEFAULT_LID) ? DEFAULT_LID : _available[0];
128
+ }
129
+
130
+ /** Directory holding the project dictionaries: env override or `<cwd>/i18x/gulp`. */
131
+ function _DictionaryDir() {
132
+ return process.env.MICROGULP_I18X_DIR || join(process.cwd(), 'i18x', 'gulp');
133
+ }
134
+
135
+ /** Languages shipped by the project (en-US source is always available). */
136
+ function _AvailableLids() {
137
+ if (availableLids) return availableLids;
138
+ let lids = [DEFAULT_LID];
139
+ try {
140
+ for (let entry of readdirSync(_DictionaryDir())) {
141
+ if (!entry.toLowerCase().endsWith('.json')) continue;
142
+ let lid = entry.slice(0, -5);
143
+ if (!lids.includes(lid)) lids.push(lid);
144
+ }
145
+ } catch {
146
+ // No dictionary directory — only the en-US source is available.
147
+ }
148
+ availableLids = lids;
149
+ return availableLids;
150
+ }
151
+
152
+ /** Resolves the active language from MICROGULP_LANG, OS locale, then default. */
153
+ function _ResolveLid() {
154
+ let wanted = process.env.MICROGULP_LANG;
155
+ if (!wanted) {
156
+ try {
157
+ wanted = Intl.DateTimeFormat().resolvedOptions().locale;
158
+ } catch {
159
+ // Intl is always present in Node 22, but stay defensive.
160
+ }
161
+ }
162
+ return _BestAvailableLid(wanted || process.env.LANG || DEFAULT_LID, _AvailableLids());
163
+ }
164
+
165
+ /** @returns {string} the active localization id (cached after first use) */
166
+ export function GetLid() {
167
+ if (!activeLid) activeLid = _ResolveLid();
168
+ return activeLid;
169
+ }
170
+
171
+ /**
172
+ * Overrides the active language explicitly and clears caches so the next
173
+ * translation reloads. Pass a falsy value to reset to automatic resolution.
174
+ * @param {string} [_lid]
175
+ */
176
+ export function SetLid(_lid) {
177
+ activeLid = _lid ? _BestAvailableLid(_lid, _AvailableLids()) : null;
178
+ }
179
+
180
+ /** Loads and caches `<dir>/<lid>.json`; a missing/broken file yields source text. */
181
+ function _LoadDictionary(_lid) {
182
+ if (dictionaryCache.has(_lid)) return dictionaryCache.get(_lid);
183
+ let dictionary = {};
184
+ try {
185
+ let filePath = join(_DictionaryDir(), _lid + '.json');
186
+ if (existsSync(filePath)) dictionary = JSON.parse(readFileSync(filePath, 'utf8'));
187
+ } catch {
188
+ // A malformed dictionary must not break the build — fall back to source.
189
+ }
190
+ dictionaryCache.set(_lid, dictionary);
191
+ return dictionary;
192
+ }
193
+
194
+ /** Substitutes `<name/>` placeholders; honors `format="…"` for int and clock times. */
195
+ function _ApplyPlaceholders(_text, _values) {
196
+ let text = String(_text);
197
+ for (let key of Object.keys(_values ?? {})) {
198
+ let pattern = new RegExp('<' + key + '(?:\\s+format="([^"]*)")?\\s*/>', 'g');
199
+ text = text.replace(pattern, (_match, _formatName) => {
200
+ if (_formatName === 'int') return String(Math.round(Number(_values[key]) || 0));
201
+ if (_formatName === 'stdtime' || _formatName === 'stddatetime') {
202
+ let date = new Date(Number(_values[key]) || 0);
203
+ let lid = GetLid();
204
+ try {
205
+ return new Intl.DateTimeFormat(lid, {
206
+ ...( _formatName === 'stddatetime'
207
+ ? { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }
208
+ : { hour: '2-digit', minute: '2-digit' }),
209
+ timeZone: GetTimeZone(),
210
+ }).format(date);
211
+ } catch {
212
+ return date.toISOString();
213
+ }
214
+ }
215
+ return String(_values[key]);
216
+ });
217
+ }
218
+ return text.replace(/<[^>]*>/g, '');
219
+ }
220
+
221
+ /**
222
+ * Translates a source phrase (including its context tag) into the active
223
+ * language and resolves placeholders. Unknown phrases fall back to the en-US
224
+ * source text with its tags stripped.
225
+ * @param {string} _text source phrase, e.g. 'Build ready<context="task log"/>'
226
+ * @param {object} [_values] placeholder values for `<name/>` tags
227
+ * @returns {string}
228
+ */
229
+ export function Translate(_text, _values) {
230
+ if (_text == null) return '';
231
+ let source = String(_text);
232
+ let lid = GetLid();
233
+ let dictionary = lid === DEFAULT_LID ? null : _LoadDictionary(lid);
234
+ let translated = (dictionary && Object.prototype.hasOwnProperty.call(dictionary, source)) ? dictionary[source] : source;
235
+ return _ApplyPlaceholders(translated, _values);
236
+ }
237
+
238
+ /**
239
+ * console.log through i18x. Prefer the `<context="task log"/>` tag.
240
+ * @param {string} _text source phrase
241
+ * @param {object} [_values]
242
+ */
243
+ export function Log(_text, _values) {
244
+ console.log(Translate(_text, _values));
245
+ }
246
+
247
+ /**
248
+ * console.warn through i18x. Prefer the `<context="task warning"/>` tag.
249
+ * @param {string} _text source phrase
250
+ * @param {object} [_values]
251
+ */
252
+ export function Warn(_text, _values) {
253
+ console.warn(Translate(_text, _values));
254
+ }
255
+
256
+ /**
257
+ * console.error through i18x. Prefer the `<context="task error"/>` tag.
258
+ * Named LogError to avoid shadowing the global Error constructor.
259
+ * @param {string} _text source phrase
260
+ * @param {object} [_values]
261
+ */
262
+ export function LogError(_text, _values) {
263
+ console.error(Translate(_text, _values));
264
+ }
265
+
266
+ /**
267
+ * µLib-compatible String-prototype API for gulpfiles that prefer the
268
+ * `"text<context=\"…\"/>".i18xTrans({ … })` ergonomics over the Log/Warn
269
+ * helpers. Installs `I18xTrans`/`i18xTrans` and `I18xRegister`/`i18xRegister`
270
+ * — but never overwrites an implementation already provided by µLib (µLib
271
+ * wins when both are loaded). Called automatically on import; also exported
272
+ * for explicit control.
273
+ */
274
+ export function InstallStringExtensions() {
275
+ if (typeof String.prototype.I18xTrans !== 'function') {
276
+ Object.defineProperty(String.prototype, 'I18xTrans', {
277
+ value: function (_values) { return Translate(String(this), _values); },
278
+ writable: true, configurable: true,
279
+ });
280
+ }
281
+ if (typeof String.prototype.i18xTrans !== 'function') {
282
+ Object.defineProperty(String.prototype, 'i18xTrans', {
283
+ value: String.prototype.I18xTrans,
284
+ writable: true, configurable: true,
285
+ });
286
+ }
287
+ if (typeof String.prototype.I18xRegister !== 'function') {
288
+ Object.defineProperty(String.prototype, 'I18xRegister', {
289
+ value: function () { return String(this); },
290
+ writable: true, configurable: true,
291
+ });
292
+ }
293
+ if (typeof String.prototype.i18xRegister !== 'function') {
294
+ Object.defineProperty(String.prototype, 'i18xRegister', {
295
+ value: String.prototype.I18xRegister,
296
+ writable: true, configurable: true,
297
+ });
298
+ }
299
+ }
300
+
301
+ InstallStringExtensions();
302
+
303
+ export default {
304
+ GetLid, SetLid, GetTimeZone, DateInTimeZone,
305
+ Translate, Log, Warn, LogError, InstallStringExtensions,
306
+ };
package/src/index.mjs CHANGED
@@ -28,6 +28,11 @@
28
28
  * @module gulp-mu-gulp-api
29
29
  */
30
30
 
31
+ // Localized console output (i18x). Re-exported so tasks import everything
32
+ // from one place: `import { Log, Warn, ReportProgress } from 'gulp-mu-gulp-api'`.
33
+ export { Log, Warn, LogError, Translate, GetLid, SetLid, GetTimeZone, DateInTimeZone, InstallStringExtensions } from './i18x.mjs';
34
+ import * as I18x from './i18x.mjs';
35
+
31
36
  let uiRequestCounter = 0;
32
37
  let ipcListenerAttached = false;
33
38
  let lastLoggedPercent = -1;
@@ -47,25 +52,20 @@ export const IsµGulp = IsMicroGulp;
47
52
  /** @deprecated Use {@link IsMicroGulp} or {@link IsµGulp}. */
48
53
  export const IsUGulp = IsMicroGulp;
49
54
 
50
- /**
51
- * Marks a µXXX task metadata string for i18x (American English source).
52
- * Appends the i18xe context tag for the property (`µDisplayName`,
53
- * `µDescription`, …) so the dashboard can translate it and a later
54
- * i18xe-server sync can pick it up. The classic gulp CLI ignores these
55
- * properties entirely.
55
+ /*
56
+ * Task metadata (µDisplayName, µDescription, µTooltip, µGroup) is localized
57
+ * the same way as any other i18x phrase: with the context tag written *into*
58
+ * the source phrase and a String.prototype.i18xRegister() marker so the
59
+ * i18xe-sync tooling extracts it and the dashboard can translate it later.
60
+ *
61
+ * MAKE_BUILDS.µDisplayName =
62
+ * 'Make Build V<version/><context="µDisplayName"/>'.i18xRegister();
56
63
  *
57
- * @param {string} _text source phrase
58
- * @param {'µDisplayName'|'µDescription'|'µTooltip'|'µGroup'} _contextProperty
59
- * @returns {string}
64
+ * i18xRegister() returns the phrase unchanged (the i18x key), so the classic
65
+ * gulp CLI keeps seeing a plain string while the dashboard translates it via
66
+ * the merged project dictionary. The prototype is installed by
67
+ * InstallStringExtensions() (auto-run on import of this module).
60
68
  */
61
- export function µMeta(_text, _contextProperty) {
62
- let text = String(_text ?? '').trim();
63
- if (!text || text.includes('<context="')) return text;
64
- return text + '<context="' + _contextProperty + '"/>';
65
- }
66
-
67
- /** ASCII export alias for {@link µMeta}. */
68
- export const MicroGulpMeta = µMeta;
69
69
 
70
70
  // -------------------------------------------------
71
71
  // progress
@@ -111,6 +111,108 @@ export function CreateProgress(_label) {
111
111
  };
112
112
  }
113
113
 
114
+ // -------------------------------------------------
115
+ // structured log output (table / tree)
116
+ // -------------------------------------------------
117
+
118
+ /**
119
+ * Sends a table to the dashboard log pane. The payload is JSON:
120
+ * `{ columns: string[] | {id, label}[], rows: (string|number)[][] | object[], hyphenate?: boolean }`.
121
+ * CLI fallback: a simple ASCII table on stdout.
122
+ *
123
+ * @param {object} _spec table descriptor
124
+ */
125
+ export function LogTable(_spec) {
126
+ let payload = _NormalizeTableSpec(_spec);
127
+ if (IsMicroGulp()) {
128
+ process.send({ type: 'structured-log', format: 'table', payload });
129
+ return;
130
+ }
131
+ console.log(_AsciiTable(payload));
132
+ }
133
+
134
+ /**
135
+ * Sends a tree structure to the dashboard log pane. The payload is JSON:
136
+ * `{ label?: string, children?: object[], roots?: object[] }` where each node
137
+ * has `{ label, children? }`. CLI fallback: indented text lines.
138
+ *
139
+ * @param {object} _spec tree descriptor (single root or `{ roots: [...] }`)
140
+ */
141
+ export function LogTree(_spec) {
142
+ let payload = _NormalizeTreeSpec(_spec);
143
+ if (IsMicroGulp()) {
144
+ process.send({ type: 'structured-log', format: 'tree', payload });
145
+ return;
146
+ }
147
+ for (let line of _AsciiTreeLines(payload)) console.log(line);
148
+ }
149
+
150
+ /**
151
+ * Sends an image gallery to the dashboard log pane. Each item needs a `src`
152
+ * (data URI or https URL) and a `label`; optional `caption` and per-item `title`.
153
+ * CLI fallback: lists labels and src lengths on stdout.
154
+ *
155
+ * @param {object} _spec `{ title?, columns?, items: { label, src, caption?, title? }[] }`
156
+ */
157
+ export function LogGallery(_spec) {
158
+ let payload = _NormalizeGallerySpec(_spec);
159
+ if (IsMicroGulp()) {
160
+ process.send({ type: 'structured-log', format: 'gallery', payload });
161
+ return;
162
+ }
163
+ console.log(_AsciiGallery(payload));
164
+ }
165
+
166
+ /**
167
+ * Renders µCSS BuildSkin `report.debug.previews` as a thumbnail gallery.
168
+ * Items without `thumbDataUri` are omitted (paths-only assets stay in the text log).
169
+ *
170
+ * @param {object[]} _previews preview entries from `report.debug.previews`
171
+ * @param {object} [_options] `{ title?, columns? }`
172
+ */
173
+ export function LogAssetPreviews(_previews, _options = {}) {
174
+ let items = (_previews ?? [])
175
+ .filter((_entry) => _entry?.thumbDataUri)
176
+ .map((_entry) => ({
177
+ label: String(_entry.relPath ?? _entry.path ?? ''),
178
+ src: _entry.thumbDataUri,
179
+ caption: [
180
+ _entry.step,
181
+ _entry.skipped ? 'cached' : null,
182
+ _entry.width && _entry.height ? `${_entry.width}\u00d7${_entry.height}` : null,
183
+ ].filter(Boolean).join(' \u00b7 '),
184
+ }));
185
+ if (!items.length) return;
186
+ LogGallery({
187
+ title: _options.title,
188
+ columns: _options.columns ?? 4,
189
+ items,
190
+ });
191
+ }
192
+
193
+ /**
194
+ * Convenience wrapper for a full µCSS `BuildSkin` debug block: summary table
195
+ * plus thumbnail gallery when `report.debug` is present.
196
+ *
197
+ * @param {object} _report BuildSkin return value
198
+ * @param {object} [_options] `{ title?, columns?, table?: boolean }`
199
+ */
200
+ export function LogBuildDebugReport(_report, _options = {}) {
201
+ let debug = _report?.debug;
202
+ if (!debug) return;
203
+ if (_options.table !== false && debug.summary) {
204
+ LogTable({
205
+ columns: [{ id: 'metric', label: 'Metric' }, { id: 'value', label: 'Value' }],
206
+ rows: Object.entries(debug.summary).map(([metric, value]) => ({ metric, value: String(value) })),
207
+ hyphenate: false,
208
+ });
209
+ }
210
+ LogAssetPreviews(debug.previews, {
211
+ title: _options.title ?? 'Generated assets',
212
+ columns: _options.columns,
213
+ });
214
+ }
215
+
114
216
  // -------------------------------------------------
115
217
  // sound & speech
116
218
  // -------------------------------------------------
@@ -333,12 +435,92 @@ async function _FallbackField(_field) {
333
435
  }
334
436
  }
335
437
 
438
+ function _NormalizeColumn(_column, _index) {
439
+ if (typeof _column === 'string') return { id: 'c' + _index, label: _column };
440
+ return { id: String(_column.id ?? 'c' + _index), label: String(_column.label ?? _column.id ?? 'c' + _index) };
441
+ }
442
+
443
+ function _NormalizeTableSpec(_spec) {
444
+ let columns = (_spec?.columns ?? []).map(_NormalizeColumn);
445
+ let rows = _spec?.rows ?? [];
446
+ let normalizedRows = rows.map((_row) => {
447
+ if (Array.isArray(_row)) {
448
+ let obj = {};
449
+ for (let c = 0; c < columns.length; c++) obj[columns[c].id] = _row[c] ?? '';
450
+ return obj;
451
+ }
452
+ return _row;
453
+ });
454
+ return { columns, rows: normalizedRows, hyphenate: _spec?.hyphenate !== false };
455
+ }
456
+
457
+ function _NormalizeTreeSpec(_spec) {
458
+ if (_spec?.roots) return { roots: _spec.roots };
459
+ if (_spec?.label != null || _spec?.children) return { roots: [_spec] };
460
+ return { roots: [] };
461
+ }
462
+
463
+ function _AsciiTable(_payload) {
464
+ let headers = _payload.columns.map((_c) => _c.label);
465
+ let widths = headers.map((_h) => _h.length);
466
+ for (let row of _payload.rows) {
467
+ for (let c = 0; c < _payload.columns.length; c++) {
468
+ let cell = String(row[_payload.columns[c].id] ?? '');
469
+ widths[c] = Math.max(widths[c], cell.length);
470
+ }
471
+ }
472
+ let sep = widths.map((_w) => '-'.repeat(_w)).join('-+-');
473
+ let lines = [headers.map((_h, _i) => _h.padEnd(widths[_i])).join(' | ')];
474
+ lines.push(sep);
475
+ for (let row of _payload.rows) {
476
+ lines.push(_payload.columns.map((_col, _i) => String(row[_col.id] ?? '').padEnd(widths[_i])).join(' | '));
477
+ }
478
+ return lines.join('\n');
479
+ }
480
+
481
+ function _AsciiTreeLines(_payload, _indent = '', _roots = null) {
482
+ let roots = _roots ?? _payload.roots ?? [];
483
+ let lines = [];
484
+ for (let i = 0; i < roots.length; i++) {
485
+ let node = roots[i];
486
+ let branch = i < roots.length - 1 ? '├─ ' : '└─ ';
487
+ lines.push(_indent + branch + String(node.label ?? ''));
488
+ let childIndent = _indent + (i < roots.length - 1 ? '│ ' : ' ');
489
+ if (node.children?.length) lines.push(..._AsciiTreeLines(_payload, childIndent, node.children));
490
+ }
491
+ return lines;
492
+ }
493
+
494
+ function _NormalizeGallerySpec(_spec) {
495
+ let columns = Math.max(1, Math.min(8, Number(_spec?.columns) || 4));
496
+ let items = (_spec?.items ?? []).map((_item) => ({
497
+ label: String(_item?.label ?? ''),
498
+ src: String(_item?.src ?? ''),
499
+ caption: _item?.caption != null ? String(_item.caption) : '',
500
+ title: _item?.title != null ? String(_item.title) : '',
501
+ })).filter((_item) => _item.src);
502
+ return {
503
+ title: _spec?.title != null ? String(_spec.title) : '',
504
+ columns,
505
+ items,
506
+ };
507
+ }
508
+
509
+ function _AsciiGallery(_payload) {
510
+ let lines = [];
511
+ if (_payload.title) lines.push(_payload.title);
512
+ for (let item of _payload.items) {
513
+ let meta = item.caption ? ` (${item.caption})` : '';
514
+ let srcHint = item.src.startsWith('data:') ? `[data URI, ${item.src.length} chars]` : item.src;
515
+ lines.push(` ${item.label}${meta}: ${srcHint}`);
516
+ }
517
+ return lines.join('\n');
518
+ }
519
+
336
520
  export default {
337
521
  IsMicroGulp,
338
522
  IsµGulp,
339
523
  IsUGulp,
340
- µMeta,
341
- MicroGulpMeta,
342
524
  ReportProgress,
343
525
  CreateProgress,
344
526
  PlaySignal,
@@ -349,4 +531,18 @@ export default {
349
531
  RequestFontInput,
350
532
  RequestSelectInput,
351
533
  RequestMultiSelectInput,
534
+ Log: I18x.Log,
535
+ Warn: I18x.Warn,
536
+ LogError: I18x.LogError,
537
+ Translate: I18x.Translate,
538
+ GetLid: I18x.GetLid,
539
+ SetLid: I18x.SetLid,
540
+ GetTimeZone: I18x.GetTimeZone,
541
+ DateInTimeZone: I18x.DateInTimeZone,
542
+ InstallStringExtensions: I18x.InstallStringExtensions,
543
+ LogTable,
544
+ LogTree,
545
+ LogGallery,
546
+ LogAssetPreviews,
547
+ LogBuildDebugReport,
352
548
  };