Skip to content

Commit 3b55f45

Browse files
feat: add math preferences (#710)
* feat(math): add MathSettings type and store defaults * feat(math): add locale-aware formatting utilities with tests * feat(math): integrate locale-aware formatting into math engine Replace hardcoded 'en-US' locale and precision 6 in formatResult with configurable activeLocale/activeDecimalPlaces backed by formatMathNumber and formatMathDate utilities. Add setFormatSettings() to the engine API. Update formatMathDate to include date+time (matching prior toLocaleString behavior) and fix tests to use explicit en-US locale. * feat(math): use locale-aware formatting for results total * feat(math): wire math settings from store to workspace and results panel * feat(math): add preferences page with i18n, routing, and navigation * fix(math): apply locale formatting to unit results and exclude dates from total
1 parent efd6687 commit 3b55f45

File tree

12 files changed

+322
-27
lines changed

12 files changed

+322
-27
lines changed

src/main/i18n/locales/en_US/preferences.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,17 @@
115115
"label": "Markdown",
116116
"codeRenderer": "Code block Renderer"
117117
},
118+
"math": {
119+
"label": "Math Notebook",
120+
"locale": {
121+
"label": "Region",
122+
"description": "Number and date display format."
123+
},
124+
"decimalPlaces": {
125+
"label": "Decimal Places",
126+
"description": "Maximum decimal places in results (0\u201314)."
127+
}
128+
},
118129
"api": {
119130
"label": "API Port",
120131
"port": {

src/main/store/module/preferences.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type {
22
EditorSettings,
33
MarkdownSettings,
4+
MathSettings,
45
NotesEditorSettings,
56
PreferencesStore,
67
} from '../types'
@@ -20,6 +21,11 @@ const isWin = platform() === 'win32'
2021

2122
const storagePath = isWin ? `${homedir()}\\massCode` : `${homedir()}/massCode`
2223

24+
const MATH_DEFAULTS: MathSettings = {
25+
locale: 'en-US',
26+
decimalPlaces: 6,
27+
}
28+
2329
const PREFERENCES_DEFAULTS: PreferencesStore = {
2430
appearance: {
2531
theme: 'auto',
@@ -41,6 +47,7 @@ const PREFERENCES_DEFAULTS: PreferencesStore = {
4147
scale: 1,
4248
},
4349
},
50+
math: MATH_DEFAULTS,
4451
}
4552

4653
function sanitizeCodeEditorSettings(value: unknown): EditorSettings {
@@ -143,6 +150,19 @@ function sanitizeMarkdownSettings(value: unknown): MarkdownSettings {
143150
}
144151
}
145152

153+
function sanitizeMathSettings(value: unknown): MathSettings {
154+
const source = asRecord(value)
155+
156+
return {
157+
locale: readString(source, 'locale', MATH_DEFAULTS.locale),
158+
decimalPlaces: readNumber(
159+
source,
160+
'decimalPlaces',
161+
MATH_DEFAULTS.decimalPlaces,
162+
),
163+
}
164+
}
165+
146166
function sanitizePreferences(value: unknown): PreferencesStore {
147167
const source = asRecord(value)
148168
const appearanceSource = asRecord(source.appearance)
@@ -162,6 +182,7 @@ function sanitizePreferences(value: unknown): PreferencesStore {
162182
= Object.keys(asRecord(editorSource.markdown)).length > 0
163183
? asRecord(editorSource.markdown)
164184
: asRecord(source.markdown)
185+
const mathSource = asRecord(source.math)
165186

166187
return {
167188
appearance: {
@@ -210,6 +231,7 @@ function sanitizePreferences(value: unknown): PreferencesStore {
210231
notes: sanitizeNotesEditorSettings(notesEditorSource),
211232
markdown: sanitizeMarkdownSettings(markdownSource),
212233
},
234+
math: sanitizeMathSettings(mathSource),
213235
}
214236
}
215237

src/main/store/types/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ export interface NotesEditorSettings {
7878
indentSize: number
7979
}
8080

81+
export interface MathSettings {
82+
locale: string
83+
decimalPlaces: number
84+
}
85+
8186
export interface PreferencesStore {
8287
appearance: {
8388
theme: string
@@ -96,6 +101,7 @@ export interface PreferencesStore {
96101
notes: NotesEditorSettings
97102
markdown: MarkdownSettings
98103
}
104+
math: MathSettings
99105
}
100106

101107
export interface MathSheet {

src/renderer/components/math-notebook/ResultsPanel.vue

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22
import type { LineResult } from '@/composables/math-notebook'
33
import { Button } from '@/components/ui/shadcn/button'
44
import { useCopyToClipboard } from '@/composables'
5+
import { formatMathNumber } from '@/composables/math-notebook/math-engine/format'
56
import { i18n, ipc } from '@/electron'
67
import { LoaderCircle, Sigma } from 'lucide-vue-next'
78
89
interface Props {
910
results: LineResult[]
1011
scrollTop: number
1112
activeLine: number
13+
locale: string
14+
decimalPlaces: number
1215
}
1316
1417
const props = defineProps<Props>()
@@ -21,7 +24,10 @@ const MATH_NOTEBOOK_DOCUMENTATION_URL
2124
const total = computed(() => {
2225
return props.results.reduce((sum, r) => {
2326
if (r.type === 'number' || r.type === 'assignment') {
24-
const num = Number.parseFloat((r.value || '').replace(/,/g, ''))
27+
const raw = r.value || ''
28+
if (raw.includes(':'))
29+
return sum
30+
const num = Number.parseFloat(raw.replace(/[^\d.\-e+]/gi, ''))
2531
if (!Number.isNaN(num))
2632
return sum + num
2733
}
@@ -30,9 +36,7 @@ const total = computed(() => {
3036
})
3137
3238
const formattedTotal = computed(() => {
33-
return Number.isInteger(total.value)
34-
? total.value.toLocaleString('en-US')
35-
: total.value.toLocaleString('en-US', { maximumFractionDigits: 6 })
39+
return formatMathNumber(total.value, props.locale, props.decimalPlaces)
3640
})
3741
3842
const resultsStyle = computed(() => {

src/renderer/components/math-notebook/Workspace.vue

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
<script setup lang="ts">
22
import type { LineResult } from '@/composables/math-notebook'
3+
import type { MathSettings } from '~/main/store/types'
34
import { useMathEngine, useMathNotebook } from '@/composables'
4-
import { i18n, ipc } from '@/electron'
5+
import { i18n, ipc, store } from '@/electron'
56
import { Calculator } from 'lucide-vue-next'
67
78
interface CurrencyRatesPayload {
@@ -10,8 +11,25 @@ interface CurrencyRatesPayload {
1011
}
1112
1213
const { activeSheet, updateSheet } = useMathNotebook()
13-
const { evaluateDocument, setCurrencyServiceState, updateCurrencyRates }
14-
= useMathEngine()
14+
const {
15+
evaluateDocument,
16+
setCurrencyServiceState,
17+
setFormatSettings,
18+
updateCurrencyRates,
19+
} = useMathEngine()
20+
21+
const mathSettings = reactive(store.preferences.get('math') as MathSettings)
22+
23+
setFormatSettings(mathSettings.locale, mathSettings.decimalPlaces)
24+
25+
watch(
26+
mathSettings,
27+
() => {
28+
store.preferences.set('math', JSON.parse(JSON.stringify(mathSettings)))
29+
setFormatSettings(mathSettings.locale, mathSettings.decimalPlaces)
30+
},
31+
{ deep: true },
32+
)
1533
1634
const scrollTop = ref(0)
1735
const activeLine = ref(0)
@@ -98,6 +116,8 @@ function handleActiveLine(line: number) {
98116
:results="results"
99117
:scroll-top="scrollTop"
100118
:active-line="activeLine"
119+
:locale="mathSettings.locale"
120+
:decimal-places="mathSettings.decimalPlaces"
101121
/>
102122
</div>
103123

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<script setup lang="ts">
2+
import type { MathSettings } from '~/main/store/types'
3+
import * as Select from '@/components/ui/shadcn/select'
4+
import {
5+
formatMathDate,
6+
formatMathNumber,
7+
} from '@/composables/math-notebook/math-engine/format'
8+
import { i18n, store } from '@/electron'
9+
10+
const settings = reactive(store.preferences.get('math') as MathSettings)
11+
12+
watch(
13+
settings,
14+
() => {
15+
store.preferences.set('math', JSON.parse(JSON.stringify(settings)))
16+
},
17+
{ deep: true },
18+
)
19+
20+
const localeOptions = [
21+
{ label: 'English (US)', value: 'en-US' },
22+
{ label: 'English (UK)', value: 'en-GB' },
23+
{ label: 'Deutsch', value: 'de-DE' },
24+
{ label: 'Fran\u00E7ais', value: 'fr-FR' },
25+
{ label: '\u0420\u0443\u0441\u0441\u043A\u0438\u0439', value: 'ru-RU' },
26+
{ label: 'Espa\u00F1ol', value: 'es-ES' },
27+
{ label: 'Portugu\u00EAs (BR)', value: 'pt-BR' },
28+
{ label: '\u65E5\u672C\u8A9E', value: 'ja-JP' },
29+
{ label: '\u4E2D\u6587', value: 'zh-CN' },
30+
{ label: '\uD55C\uAD6D\uC5B4', value: 'ko-KR' },
31+
{ label: 'Italiano', value: 'it-IT' },
32+
{ label: 'Polski', value: 'pl-PL' },
33+
{
34+
label: '\u0423\u043A\u0440\u0430\u0457\u043D\u0441\u044C\u043A\u0430',
35+
value: 'uk-UA',
36+
},
37+
{ label: 'T\u00FCrk\u00E7e', value: 'tr-TR' },
38+
{ label: 'Nederlands', value: 'nl-NL' },
39+
]
40+
41+
const decimalPlacesOptions = Array.from({ length: 15 }, (_, i) => ({
42+
label: String(i),
43+
value: String(i),
44+
}))
45+
46+
const localePreview = computed(() => {
47+
const num = formatMathNumber(
48+
1234.5678,
49+
settings.locale,
50+
settings.decimalPlaces,
51+
)
52+
const date = formatMathDate(new Date(), settings.locale)
53+
return `${num} \u00B7 ${date}`
54+
})
55+
56+
const decimalPlacesPreview = computed(() => {
57+
return `1/3 = ${formatMathNumber(1 / 3, settings.locale, settings.decimalPlaces)}`
58+
})
59+
</script>
60+
61+
<template>
62+
<div class="space-y-4">
63+
<UiMenuFormSection :label="i18n.t('preferences:math.label')">
64+
<UiMenuFormItem :label="i18n.t('preferences:math.locale.label')">
65+
<Select.Select v-model="settings.locale">
66+
<Select.SelectTrigger class="w-64">
67+
<Select.SelectValue />
68+
</Select.SelectTrigger>
69+
<Select.SelectContent>
70+
<Select.SelectItem
71+
v-for="option in localeOptions"
72+
:key="option.value"
73+
:value="option.value"
74+
>
75+
{{ option.label }}
76+
</Select.SelectItem>
77+
</Select.SelectContent>
78+
</Select.Select>
79+
<template #description>
80+
{{ i18n.t("preferences:math.locale.description") }}
81+
{{ localePreview }}
82+
</template>
83+
</UiMenuFormItem>
84+
<UiMenuFormItem :label="i18n.t('preferences:math.decimalPlaces.label')">
85+
<Select.Select
86+
:model-value="String(settings.decimalPlaces)"
87+
@update:model-value="settings.decimalPlaces = Number($event)"
88+
>
89+
<Select.SelectTrigger class="w-64">
90+
<Select.SelectValue />
91+
</Select.SelectTrigger>
92+
<Select.SelectContent>
93+
<Select.SelectItem
94+
v-for="option in decimalPlacesOptions"
95+
:key="option.value"
96+
:value="option.value"
97+
>
98+
{{ option.label }}
99+
</Select.SelectItem>
100+
</Select.SelectContent>
101+
</Select.Select>
102+
<template #description>
103+
{{ i18n.t("preferences:math.decimalPlaces.description") }}
104+
{{ decimalPlacesPreview }}
105+
</template>
106+
</UiMenuFormItem>
107+
</UiMenuFormSection>
108+
</div>
109+
</template>

src/renderer/composables/__tests__/useMathEngine.test.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -543,7 +543,7 @@ describe('prev', () => {
543543
const results = evalLines('fromunix(1446587186)\nprev + 1 day')
544544
expect(results[1].type).toBe('date')
545545
expect(results[1].value).toBe(
546-
new Date((1446587186 + 86400) * 1000).toLocaleString(),
546+
new Date((1446587186 + 86400) * 1000).toLocaleString('en-US'),
547547
)
548548
})
549549
})
@@ -658,7 +658,7 @@ describe('fromunix', () => {
658658
const result = evalLine('fromunix(1446587186) + 2 day')
659659
expect(result.type).toBe('date')
660660
expect(result.value).toBe(
661-
new Date((1446587186 + 2 * 86400) * 1000).toLocaleString(),
661+
new Date((1446587186 + 2 * 86400) * 1000).toLocaleString('en-US'),
662662
)
663663
})
664664

@@ -667,16 +667,20 @@ describe('fromunix', () => {
667667
expect(results[0].type).toBe('assignment')
668668
expect(results[1].type).toBe('date')
669669
expect(results[1].value).toBe(
670-
new Date((1446587186 + 2 * 86400) * 1000).toLocaleString(),
670+
new Date((1446587186 + 2 * 86400) * 1000).toLocaleString('en-US'),
671671
)
672672
})
673673

674674
it('local dotted date assignment + 2 day', () => {
675675
const results = evalLines('x = 12.03.2025\nx + 2 day')
676676
expect(results[0].type).toBe('assignment')
677-
expect(results[0].value).toBe(new Date(2025, 2, 12).toLocaleString())
677+
expect(results[0].value).toBe(
678+
new Date(2025, 2, 12).toLocaleString('en-US'),
679+
)
678680
expect(results[1].type).toBe('date')
679-
expect(results[1].value).toBe(new Date(2025, 2, 14).toLocaleString())
681+
expect(results[1].value).toBe(
682+
new Date(2025, 2, 14).toLocaleString('en-US'),
683+
)
680684
})
681685
})
682686

@@ -706,7 +710,7 @@ describe('time zones', () => {
706710
const result = evalLine('time() + 1 day')
707711
expect(result.type).toBe('date')
708712
expect(result.value).toBe(
709-
new Date('2026-03-07T12:00:00Z').toLocaleString(),
713+
new Date('2026-03-07T12:00:00Z').toLocaleString('en-US'),
710714
)
711715
})
712716

@@ -720,7 +724,7 @@ describe('time zones', () => {
720724
const result = evalLine('now + 1 day')
721725
expect(result.type).toBe('date')
722726
expect(result.value).toBe(
723-
new Date('2026-03-07T12:00:00Z').toLocaleString(),
727+
new Date('2026-03-07T12:00:00Z').toLocaleString('en-US'),
724728
)
725729
})
726730

0 commit comments

Comments
 (0)