calculation-breakdown 0.1.1 → 0.1.2

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 CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  Notable changes, starting with 0.1.0. The latest five releases are detailed here; older releases are archived.
4
4
 
5
+ ## 0.1.2 — 2026-10-26
6
+
7
+ - Added an opt-in locale mode to the playground that preserves calculation edits, keeps the default example limited to the calculation, and falls back to semantic keys without generated dictionary placeholders.
8
+ - Made the playground vertically scrollable and its three panels usable on mobile screens.
9
+ - Fixed the deployed playground by including every browser ESM module, and kept release artifact inspection JSON-safe by suppressing duplicate package lifecycle scripts.
10
+
5
11
  ## 0.1.1 — 2026-10-26
6
12
 
7
13
  - Added the guarded npm, Git tag, release-PR, and GitHub Pages deployment workflow used by `payload-vars`.
@@ -0,0 +1,242 @@
1
+ import { CalculationBreakdownError } from './calculation-breakdown.error.js';
2
+ import { toHumanString, } from './calculation-breakdown-human-string.js';
3
+ const DECIMAL_PATTERN = /^-?(?:0|[1-9]\d*)(?:\.\d+)?$/;
4
+ const CURRENCY_PATTERN = /^[A-Z]{3}$/;
5
+ const DURATION_UNITS = new Set([
6
+ 'milliseconds',
7
+ 'seconds',
8
+ 'minutes',
9
+ 'hours',
10
+ 'days',
11
+ ]);
12
+ const ROUNDING_MODES = new Set([
13
+ 'up',
14
+ 'down',
15
+ 'nearest',
16
+ ]);
17
+ const GROUP_ROLES = new Set([
18
+ 'detail',
19
+ 'summary',
20
+ ]);
21
+ const GROUP_ORIENTATIONS = new Set([
22
+ 'horizontal',
23
+ 'vertical',
24
+ ]);
25
+ const OPERATORS = new Set([
26
+ 'add',
27
+ 'subtract',
28
+ 'multiply',
29
+ 'divide',
30
+ 'minimum',
31
+ 'maximum',
32
+ ]);
33
+ const OPERATOR_ALIASES = {
34
+ sub: 'subtract',
35
+ mul: 'multiply',
36
+ div: 'divide',
37
+ min: 'minimum',
38
+ max: 'maximum',
39
+ };
40
+ function assertSemantic(semantic) {
41
+ if (semantic.trim().length === 0) {
42
+ throw new CalculationBreakdownError('INVALID_SEMANTIC', 'Semantic must be a non-empty string.');
43
+ }
44
+ }
45
+ function assertDecimal(value) {
46
+ if (!DECIMAL_PATTERN.test(value)) {
47
+ throw new CalculationBreakdownError('INVALID_DECIMAL', `Invalid decimal string: ${value}`);
48
+ }
49
+ }
50
+ function assertDurationOptions(options) {
51
+ if (!options)
52
+ return;
53
+ const hasRounding = options.rounding !== undefined;
54
+ const hasStep = options.step !== undefined;
55
+ const step = options.step;
56
+ if (hasRounding &&
57
+ !ROUNDING_MODES.has(options.rounding)) {
58
+ throw new CalculationBreakdownError('INVALID_ROUNDING', `Invalid rounding mode: ${String(options.rounding)}`);
59
+ }
60
+ if (hasRounding !== hasStep) {
61
+ throw new CalculationBreakdownError('INVALID_ROUNDING_STEP', 'Rounding and step must be provided together.');
62
+ }
63
+ if (hasStep &&
64
+ (step === undefined || !Number.isFinite(step) || step <= 0)) {
65
+ throw new CalculationBreakdownError('INVALID_ROUNDING_STEP', 'Rounding step must be a positive finite number.');
66
+ }
67
+ }
68
+ function deepFreeze(value) {
69
+ if (typeof value !== 'object' || value === null || Object.isFrozen(value)) {
70
+ return value;
71
+ }
72
+ Object.freeze(value);
73
+ for (const child of Object.values(value))
74
+ deepFreeze(child);
75
+ return value;
76
+ }
77
+ function cloneJson(value) {
78
+ return JSON.parse(JSON.stringify(value));
79
+ }
80
+ /**
81
+ * Records a typed, serializable breakdown of a calculation performed elsewhere.
82
+ *
83
+ * This builder does not evaluate operations or calculate the result. The calling
84
+ * application supplies every value, including the authoritative result.
85
+ *
86
+ * @typeParam Semantic - String union used to label the domain meaning of values.
87
+ *
88
+ * @example
89
+ * ```ts
90
+ * const breakdown = new CalculationBreakdownBuilder<'price' | 'total'>()
91
+ * .breakdown(({ money, constant, multiply }) =>
92
+ * multiply(money('price', '100.00', 'USD'), constant('0.9')),
93
+ * )
94
+ * .result(({ money }) => money('total', '90.00', 'USD'))
95
+ * .build();
96
+ * ```
97
+ */
98
+ export class CalculationBreakdownBuilder {
99
+ breakdownValue;
100
+ resultValue;
101
+ nodeBuilder = {
102
+ money: (semantic, amount, currency) => this.createMoney(semantic, amount, currency),
103
+ duration: (semantic, amount, unit, options) => this.createDuration(semantic, amount, unit, options),
104
+ number: (semantic, value) => this.createNumber(semantic, value),
105
+ percent: (semantic, value) => this.createPercent(semantic, value),
106
+ constant: (value) => this.createConstant(value),
107
+ group: (expression, options) => this.createGroup(expression, options),
108
+ operator: (left, operator, right) => this.createOperation(left, operator, right),
109
+ add: (left, right) => this.createOperation(left, 'add', right),
110
+ subtract: (left, right) => this.createOperation(left, 'subtract', right),
111
+ sub: (left, right) => this.createOperation(left, 'sub', right),
112
+ multiply: (left, right) => this.createOperation(left, 'multiply', right),
113
+ mul: (left, right) => this.createOperation(left, 'mul', right),
114
+ divide: (left, right) => this.createOperation(left, 'divide', right),
115
+ div: (left, right) => this.createOperation(left, 'div', right),
116
+ minimum: (left, right) => this.createOperation(left, 'minimum', right),
117
+ min: (left, right) => this.createOperation(left, 'min', right),
118
+ maximum: (left, right) => this.createOperation(left, 'maximum', right),
119
+ max: (left, right) => this.createOperation(left, 'max', right),
120
+ };
121
+ /**
122
+ * Sets the root node that describes the calculation.
123
+ *
124
+ * @param create - Callback that describes the calculation using typed factories.
125
+ * @returns This builder for fluent chaining.
126
+ */
127
+ breakdown(create) {
128
+ this.breakdownValue = create(this.nodeBuilder);
129
+ return this;
130
+ }
131
+ /**
132
+ * Sets the authoritative result calculated by the calling application.
133
+ *
134
+ * @param create - Callback that constructs a money, duration, or number value.
135
+ * @returns This builder for fluent chaining.
136
+ */
137
+ result(create) {
138
+ this.resultValue = create(this.nodeBuilder);
139
+ return this;
140
+ }
141
+ /**
142
+ * Produces a detached, deeply frozen calculation breakdown.
143
+ *
144
+ * @throws {CalculationBreakdownError} If either the breakdown or result is missing.
145
+ */
146
+ build() {
147
+ if (!this.breakdownValue) {
148
+ throw new CalculationBreakdownError('MISSING_BREAKDOWN', 'A breakdown is required before building.');
149
+ }
150
+ if (!this.resultValue) {
151
+ throw new CalculationBreakdownError('MISSING_RESULT', 'A result is required before building.');
152
+ }
153
+ return deepFreeze(cloneJson({
154
+ breakdown: this.breakdownValue,
155
+ result: this.resultValue,
156
+ }));
157
+ }
158
+ /** Builds and renders the calculation using localized semantic labels. */
159
+ toHumanString(options) {
160
+ return toHumanString(this.build(), options);
161
+ }
162
+ createMoney(semantic, amount, currency) {
163
+ assertSemantic(semantic);
164
+ assertDecimal(amount);
165
+ if (!CURRENCY_PATTERN.test(currency)) {
166
+ throw new CalculationBreakdownError('INVALID_CURRENCY', `Currency must be a three-letter uppercase code: ${currency}`);
167
+ }
168
+ return deepFreeze({ type: 'money', semantic, amount, currency });
169
+ }
170
+ createDuration(semantic, amount, unit, options) {
171
+ assertSemantic(semantic);
172
+ assertDecimal(amount);
173
+ if (!DURATION_UNITS.has(unit)) {
174
+ throw new CalculationBreakdownError('INVALID_DURATION_UNIT', `Unsupported duration unit: ${unit}`);
175
+ }
176
+ assertDurationOptions(options);
177
+ return deepFreeze({
178
+ type: 'duration',
179
+ semantic,
180
+ amount,
181
+ unit,
182
+ ...(options?.rounding ? { rounding: options.rounding } : {}),
183
+ ...(options?.step !== undefined ? { step: options.step } : {}),
184
+ });
185
+ }
186
+ createNumber(semantic, value) {
187
+ assertSemantic(semantic);
188
+ assertDecimal(value);
189
+ return deepFreeze({ type: 'number', semantic, value });
190
+ }
191
+ createPercent(semantic, value) {
192
+ assertSemantic(semantic);
193
+ assertDecimal(value);
194
+ return deepFreeze({ type: 'percent', semantic, value });
195
+ }
196
+ createConstant(value) {
197
+ assertDecimal(value);
198
+ return deepFreeze({ type: 'constant', value });
199
+ }
200
+ createGroup(expression, options) {
201
+ if (options?.semantic !== undefined) {
202
+ assertSemantic(options.semantic);
203
+ }
204
+ if (options?.role !== undefined &&
205
+ !GROUP_ROLES.has(options.role)) {
206
+ throw new CalculationBreakdownError('INVALID_GROUP_ROLE', `Unsupported group role: ${String(options.role)}`);
207
+ }
208
+ if (options?.orientation !== undefined &&
209
+ !GROUP_ORIENTATIONS.has(options.orientation)) {
210
+ throw new CalculationBreakdownError('INVALID_GROUP_ORIENTATION', `Unsupported group orientation: ${String(options.orientation)}`);
211
+ }
212
+ return deepFreeze({
213
+ type: 'group',
214
+ expression,
215
+ ...(options?.wrap === true ? { wrap: true } : {}),
216
+ ...(options?.semantic !== undefined
217
+ ? { semantic: options.semantic }
218
+ : {}),
219
+ ...(options?.result !== undefined
220
+ ? { result: options.result }
221
+ : {}),
222
+ ...(options?.role !== undefined
223
+ ? { role: options.role }
224
+ : {}),
225
+ ...(options?.orientation !== undefined
226
+ ? { orientation: options.orientation }
227
+ : {}),
228
+ });
229
+ }
230
+ createOperation(left, operator, right) {
231
+ const normalizedOperator = OPERATOR_ALIASES[operator] ?? operator;
232
+ if (!OPERATORS.has(normalizedOperator)) {
233
+ throw new CalculationBreakdownError('INVALID_OPERATOR', `Unsupported operator: ${operator}`);
234
+ }
235
+ return deepFreeze({
236
+ type: 'operation',
237
+ operator: normalizedOperator,
238
+ operands: [left, right],
239
+ });
240
+ }
241
+ }
242
+ //# sourceMappingURL=calculation-breakdown-builder.js.map
@@ -0,0 +1,86 @@
1
+ const INFIX_OPERATORS = {
2
+ add: '+',
3
+ subtract: '−',
4
+ multiply: '×',
5
+ divide: '÷',
6
+ };
7
+ function localizeDecimal(value, locale) {
8
+ return locale === 'cs' ? value.replace('.', ',') : value;
9
+ }
10
+ function decimalToPercent(value) {
11
+ const negative = value.startsWith('-');
12
+ const unsigned = negative ? value.slice(1) : value;
13
+ const [whole = '0', fraction = ''] = unsigned.split('.');
14
+ const digits = `${whole}${fraction}`;
15
+ const decimalIndex = whole.length + 2;
16
+ const padded = digits.padEnd(decimalIndex, '0');
17
+ const integer = padded
18
+ .slice(0, decimalIndex)
19
+ .replace(/^0+(?=\d)/, '');
20
+ const decimals = padded.slice(decimalIndex).replace(/0+$/, '');
21
+ return `${negative ? '-' : ''}${integer}${decimals ? `.${decimals}` : ''}`;
22
+ }
23
+ function localizeDurationUnit(amount, unit, locale) {
24
+ const singularUnit = unit.slice(0, -1);
25
+ const unitPart = new Intl.NumberFormat(locale, {
26
+ style: 'unit',
27
+ unit: singularUnit,
28
+ unitDisplay: 'long',
29
+ useGrouping: false,
30
+ })
31
+ .formatToParts(Number(amount))
32
+ .find((part) => part.type === 'unit');
33
+ if (!unitPart) {
34
+ throw new Error(`Unable to localize duration unit: ${unit}`);
35
+ }
36
+ return unitPart.value;
37
+ }
38
+ function labelValue(value, semantic, options) {
39
+ if (options.includeNames === false)
40
+ return value;
41
+ const entry = options.dictionary[options.locale]?.[semantic];
42
+ const label = typeof entry === 'string' ? entry : (entry?.name ?? semantic);
43
+ return `${value} ${label}`;
44
+ }
45
+ function renderValue(value, options) {
46
+ const rawDecimal = value.type === 'number' || value.type === 'percent'
47
+ ? value.value
48
+ : value.amount;
49
+ const decimal = localizeDecimal(rawDecimal, options.locale);
50
+ if (value.type === 'money') {
51
+ return labelValue(`${decimal} ${value.currency}`, value.semantic, options);
52
+ }
53
+ if (value.type === 'duration') {
54
+ return labelValue(`${decimal} ${localizeDurationUnit(value.amount, value.unit, options.locale)}`, value.semantic, options);
55
+ }
56
+ if (value.type === 'percent') {
57
+ return labelValue(`${localizeDecimal(decimalToPercent(value.value), options.locale)}%`, value.semantic, options);
58
+ }
59
+ return labelValue(decimal, value.semantic, options);
60
+ }
61
+ function renderNode(node, options, nested = false) {
62
+ if (node.type === 'constant') {
63
+ return localizeDecimal(node.value, options.locale);
64
+ }
65
+ if (node.type === 'group') {
66
+ const result = node.result
67
+ ? ` = ${renderValue(node.result, options)}`
68
+ : '';
69
+ return `(${renderNode(node.expression, options)}${result})`;
70
+ }
71
+ if (node.type !== 'operation') {
72
+ return renderValue(node, options);
73
+ }
74
+ const [left, right] = node.operands;
75
+ if (node.operator === 'minimum' || node.operator === 'maximum') {
76
+ const functionName = node.operator === 'minimum' ? 'min' : 'max';
77
+ return `${functionName}(${renderNode(left, options)}, ${renderNode(right, options)})`;
78
+ }
79
+ const rendered = `${renderNode(left, options, true)} ${INFIX_OPERATORS[node.operator]} ${renderNode(right, options, true)}`;
80
+ return nested ? `(${rendered})` : rendered;
81
+ }
82
+ /** Renders a plain calculation breakdown without changing its serialized form. */
83
+ export function toHumanString(calculation, options) {
84
+ return `${renderNode(calculation.breakdown, options)} = ${renderValue(calculation.result, options)}`;
85
+ }
86
+ //# sourceMappingURL=calculation-breakdown-human-string.js.map
@@ -0,0 +1,16 @@
1
+ /** Error thrown when a calculation breakdown contains invalid or incomplete input. */
2
+ export class CalculationBreakdownError extends Error {
3
+ code;
4
+ /**
5
+ * Creates a calculation breakdown error.
6
+ *
7
+ * @param code - Stable code suitable for programmatic error handling.
8
+ * @param message - Human-readable explanation of the failure.
9
+ */
10
+ constructor(code, message) {
11
+ super(message);
12
+ this.code = code;
13
+ this.name = 'CalculationBreakdownError';
14
+ }
15
+ }
16
+ //# sourceMappingURL=calculation-breakdown.error.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=calculation-breakdown.interface.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=calculation-breakdown.type.js.map
package/docs/changelog.md CHANGED
@@ -8,6 +8,12 @@ title: Changelog
8
8
 
9
9
  Notable changes, starting with 0.1.0. The latest five releases are detailed here; older releases are archived.
10
10
 
11
+ ## 0.1.2 — 2026-10-26
12
+
13
+ - Added an opt-in locale mode to the playground that preserves calculation edits, keeps the default example limited to the calculation, and falls back to semantic keys without generated dictionary placeholders.
14
+ - Made the playground vertically scrollable and its three panels usable on mobile screens.
15
+ - Fixed the deployed playground by including every browser ESM module, and kept release artifact inspection JSON-safe by suppressing duplicate package lifecycle scripts.
16
+
11
17
  ## 0.1.1 — 2026-10-26
12
18
 
13
19
  - Added the guarded npm, Git tag, release-PR, and GitHub Pages deployment workflow used by `payload-vars`.
package/docs/index.md CHANGED
@@ -6,7 +6,7 @@ title: calculation-breakdown
6
6
 
7
7
  # calculation-breakdown
8
8
 
9
- Package version: v0.1.1
9
+ Package version: v0.1.2
10
10
 
11
11
  Typed, serializable breakdowns of calculations performed elsewhere.
12
12
 
@@ -17,6 +17,10 @@
17
17
  <span class="badge">playground</span>
18
18
  </div>
19
19
  <div class="actions">
20
+ <label class="option-control">
21
+ <input id="use-locale" type="checkbox" />
22
+ <span>Use locale</span>
23
+ </label>
20
24
  <button id="reset" class="button button-secondary" type="button">
21
25
  Reset
22
26
  </button>
@@ -67,6 +67,22 @@ body {
67
67
 
68
68
  .locale-control { color: var(--muted); font-size: 12px; }
69
69
 
70
+ .option-control {
71
+ display: inline-flex;
72
+ align-items: center;
73
+ gap: 7px;
74
+ color: var(--muted);
75
+ font-size: 12px;
76
+ cursor: pointer;
77
+ }
78
+
79
+ .option-control input {
80
+ width: 15px;
81
+ height: 15px;
82
+ margin: 0;
83
+ accent-color: var(--accent);
84
+ }
85
+
70
86
  select,
71
87
  .button {
72
88
  height: 36px;
@@ -274,6 +290,8 @@ textarea:focus { box-shadow: inset 0 0 0 1px #596927; }
274
290
  background: #e9ecdf;
275
291
  }
276
292
 
293
+ .language-switcher[hidden] { display: none; }
294
+
277
295
  .view-switcher {
278
296
  grid-template-columns: repeat(3, 1fr);
279
297
  }
@@ -363,12 +381,27 @@ textarea:focus { box-shadow: inset 0 0 0 1px #596927; }
363
381
  }
364
382
 
365
383
  @media (max-width: 650px) {
384
+ html,
385
+ body {
386
+ height: auto;
387
+ min-height: 100%;
388
+ overflow-x: hidden;
389
+ overflow-y: auto;
390
+ -webkit-overflow-scrolling: touch;
391
+ }
392
+
366
393
  .topbar { align-items: flex-start; flex-direction: column; }
367
394
  .actions { width: 100%; flex-wrap: wrap; }
368
395
  .workspace {
396
+ flex: none;
369
397
  grid-template-columns: 1fr;
370
- grid-template-rows: repeat(3, minmax(0, 1fr));
398
+ grid-template-rows: none;
399
+ grid-auto-rows: max(560px, 80dvh);
371
400
  }
372
401
  .preview-panel { grid-column: auto; }
373
- .panel { border-right: 0; border-bottom: 1px solid var(--line); }
402
+ .panel {
403
+ min-height: 560px;
404
+ border-right: 0;
405
+ border-bottom: 1px solid var(--line);
406
+ }
374
407
  }
@@ -3,7 +3,7 @@ import {
3
3
  toHumanString,
4
4
  } from '../assets/calculation-breakdown/index.js';
5
5
 
6
- const DEFAULT_BUILDER = `// Derive calendar-day counts in UTC to avoid daylight-saving offsets.
6
+ const DEFAULT_BUILDER_WITH_LOCALE = `// Derive calendar-day counts in UTC to avoid daylight-saving offsets.
7
7
  const helper = {
8
8
  now: new Date(),
9
9
  get year() { return this.now.getFullYear(); },
@@ -179,6 +179,35 @@ return {
179
179
  dictionary, // Optional: comment out this line to use semantic keys directly.
180
180
  };`;
181
181
 
182
+ const localeMetadataStart = DEFAULT_BUILDER_WITH_LOCALE.indexOf(
183
+ '\n// Optional localized title',
184
+ );
185
+ const DEFAULT_BUILDER_CORE = DEFAULT_BUILDER_WITH_LOCALE.slice(
186
+ 0,
187
+ localeMetadataStart,
188
+ );
189
+ const DEFAULT_LOCALE_METADATA = DEFAULT_BUILDER_WITH_LOCALE.slice(
190
+ localeMetadataStart,
191
+ );
192
+ const DEFAULT_MINIMAL_EXPORT = `
193
+
194
+ // Export only the calculation; renderers fall back to its semantic keys.
195
+ return { calculation: invoice };`;
196
+ const DEFAULT_BUILDER = `${DEFAULT_BUILDER_CORE}${DEFAULT_MINIMAL_EXPORT}`;
197
+
198
+ function setBuilderLocaleMode(source, useLocale) {
199
+ const localizedStart = source.indexOf('\n// Optional localized title');
200
+ const hasMinimalExport = source.endsWith(DEFAULT_MINIMAL_EXPORT);
201
+ const core = localizedStart >= 0
202
+ ? source.slice(0, localizedStart)
203
+ : hasMinimalExport
204
+ ? source.slice(0, -DEFAULT_MINIMAL_EXPORT.length)
205
+ : source;
206
+
207
+ if (localizedStart < 0 && !hasMinimalExport) return source;
208
+ return `${core}${useLocale ? DEFAULT_LOCALE_METADATA : DEFAULT_MINIMAL_EXPORT}`;
209
+ }
210
+
182
211
  const DEFAULT_CSS = `.calculation-card__expression {
183
212
  font: 650 14px/1.45 Inter, system-ui, sans-serif;
184
213
  white-space: nowrap;
@@ -325,6 +354,7 @@ const elements = {
325
354
  status: document.querySelector('#status'),
326
355
  run: document.querySelector('#run'),
327
356
  reset: document.querySelector('#reset'),
357
+ useLocale: document.querySelector('#use-locale'),
328
358
  };
329
359
 
330
360
  let updateTimer;
@@ -1114,10 +1144,16 @@ function render() {
1114
1144
  hideTooltip();
1115
1145
  const output = evaluateBuilder(elements.builder.value);
1116
1146
  const locale = selectedLocale;
1117
- const dictionary = output.dictionary ?? {};
1147
+ const useLocale = elements.useLocale.checked;
1148
+ const dictionary = useLocale ? output.dictionary ?? {} : {};
1149
+ const hasLocalization = useLocale && Boolean(
1150
+ output.title || output.dictionary,
1151
+ );
1118
1152
  const examples = output.examples ?? [
1119
1153
  {
1120
- title: output.title ?? { en: 'Calculation', cs: 'Výpočet' },
1154
+ title: useLocale
1155
+ ? output.title ?? { en: 'Calculation', cs: 'Výpočet' }
1156
+ : undefined,
1121
1157
  calculation: output.calculation,
1122
1158
  },
1123
1159
  ];
@@ -1128,6 +1164,10 @@ function render() {
1128
1164
  const fragment = elements.template.content.cloneNode(true);
1129
1165
  const calculation = example.calculation;
1130
1166
  const entries = createSemanticEntries(output, calculation, locale);
1167
+ const languageSwitcher = fragment.querySelector(
1168
+ '.language-switcher',
1169
+ );
1170
+ languageSwitcher.hidden = !hasLocalization;
1131
1171
 
1132
1172
  for (const button of fragment.querySelectorAll('[data-locale]')) {
1133
1173
  const isActive = button.dataset.locale === locale;
@@ -1170,7 +1210,9 @@ function render() {
1170
1210
  fragment.querySelector('[data-result-type]').textContent =
1171
1211
  calculation.result.type;
1172
1212
  const includeNames = selectedView === 'formula-names';
1173
- const command = `toHumanString(calculation, { locale: '${locale}', dictionary, includeNames: ${includeNames} })`;
1213
+ const command = useLocale
1214
+ ? `toHumanString(calculation, { locale: '${locale}', dictionary, includeNames: ${includeNames} })`
1215
+ : `toHumanString(calculation, { includeNames: ${includeNames} })`;
1174
1216
  const commandElement = fragment.querySelector('[data-command]');
1175
1217
  commandElement.hidden = selectedView === 'table';
1176
1218
  commandElement.innerHTML = highlight(
@@ -1214,6 +1256,7 @@ function scheduleRender() {
1214
1256
  }
1215
1257
 
1216
1258
  function reset() {
1259
+ elements.useLocale.checked = false;
1217
1260
  elements.builder.value = DEFAULT_BUILDER;
1218
1261
  elements.css.value = DEFAULT_CSS;
1219
1262
  selectedLocale = 'en';
@@ -1245,6 +1288,15 @@ for (const editor of [elements.builder, elements.css]) {
1245
1288
 
1246
1289
  elements.run.addEventListener('click', render);
1247
1290
  elements.reset.addEventListener('click', reset);
1291
+ elements.useLocale.addEventListener('change', () => {
1292
+ selectedLocale = 'en';
1293
+ elements.builder.value = setBuilderLocaleMode(
1294
+ elements.builder.value,
1295
+ elements.useLocale.checked,
1296
+ );
1297
+ updateHighlights();
1298
+ render();
1299
+ });
1248
1300
  elements.preview.addEventListener('scroll', hideTooltip, { passive: true });
1249
1301
  window.addEventListener('resize', hideTooltip, { passive: true });
1250
1302
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "calculation-breakdown",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Typed, serializable breakdowns of calculations performed by financial and billing systems",
5
5
  "type": "module",
6
6
  "license": "MIT",