support changing numeral system

This commit is contained in:
MaysWind
2025-08-17 01:55:19 +08:00
parent ab6d4ee6fc
commit cd4d230d29
59 changed files with 1153 additions and 582 deletions
@@ -47,7 +47,7 @@ interface AccountBalanceTrendsChartDataItem {
const props = defineProps<DesktopAccountBalanceTrendsChartProps>();
const theme = useTheme();
const { tt, formatAmountWithCurrency } = useI18n();
const { tt, formatAmountToLocalizedNumeralsWithCurrency } = useI18n();
const { allDataItems, allDisplayDateRanges } = useAccountBalanceTrendsChartBase(props);
const userStore = useUserStore();
@@ -129,8 +129,8 @@ const yAxisWidth = computed<number>(() => {
}
}
const maxValueText = formatAmountWithCurrency(maxValue, props.account.currency);
const minValueText = formatAmountWithCurrency(minValue, props.account.currency);
const maxValueText = formatAmountToLocalizedNumeralsWithCurrency(maxValue, props.account.currency);
const minValueText = formatAmountToLocalizedNumeralsWithCurrency(minValue, props.account.currency);
const maxLengthText = maxValueText.length > minValueText.length ? maxValueText : minValueText;
const canvas = document.createElement('canvas');
@@ -175,27 +175,27 @@ const chartOptions = computed<object>(() => {
const displayItems: NameValue[] = [
{
name: tt('Opening Balance'),
value: formatAmountWithCurrency(dataItem.openingBalance, props.account.currency)
value: formatAmountToLocalizedNumeralsWithCurrency(dataItem.openingBalance, props.account.currency)
},
{
name: tt('Closing Balance'),
value: formatAmountWithCurrency(dataItem.closingBalance, props.account.currency)
value: formatAmountToLocalizedNumeralsWithCurrency(dataItem.closingBalance, props.account.currency)
},
{
name: tt('Minimum Balance'),
value: formatAmountWithCurrency(dataItem.minimumBalance, props.account.currency)
value: formatAmountToLocalizedNumeralsWithCurrency(dataItem.minimumBalance, props.account.currency)
},
{
name: tt('Maximum Balance'),
value: formatAmountWithCurrency(dataItem.maximumBalance, props.account.currency)
value: formatAmountToLocalizedNumeralsWithCurrency(dataItem.maximumBalance, props.account.currency)
},
{
name: tt('Median Balance'),
value: formatAmountWithCurrency(dataItem.medianBalance, props.account.currency)
value: formatAmountToLocalizedNumeralsWithCurrency(dataItem.medianBalance, props.account.currency)
},
{
name: tt('Average Balance'),
value: formatAmountWithCurrency(dataItem.averageBalance, props.account.currency)
value: formatAmountToLocalizedNumeralsWithCurrency(dataItem.averageBalance, props.account.currency)
}
];
@@ -210,7 +210,7 @@ const chartOptions = computed<object>(() => {
return tooltip;
} else {
const amount = params[0].data as number;
const value = formatAmountWithCurrency(amount, props.account.currency);
const value = formatAmountToLocalizedNumeralsWithCurrency(amount, props.account.currency);
return `${params[0].name}<br/>`
+ '<div><span class="chart-pointer" style="background-color: #' + DEFAULT_CHART_COLORS[0] + '"></span>'
@@ -234,13 +234,13 @@ const chartOptions = computed<object>(() => {
type: 'value',
axisLabel: {
formatter: (value: string) => {
return formatAmountWithCurrency(value, props.account.currency);
return formatAmountToLocalizedNumeralsWithCurrency(parseInt(value), props.account.currency);
}
},
axisPointer: {
label: {
formatter: (params: CallbackDataParams) => {
return formatAmountWithCurrency(Math.floor(params.value as number), props.account.currency);
return formatAmountToLocalizedNumeralsWithCurrency(Math.floor(params.value as number), props.account.currency);
}
}
},
+47 -23
View File
@@ -74,11 +74,11 @@ import { ref, computed, useTemplateRef, watch } from 'vue';
import { useI18n } from '@/locales/helpers.ts';
import { type CommonNumberInputProps, useCommonNumberInputBase } from '@/components/base/CommonNumberInputBase.ts';
import { DecimalSeparator } from '@/core/numeral.ts';
import { NumeralSystem, DecimalSeparator } from '@/core/numeral.ts';
import type { CurrencyPrependAndAppendText } from '@/core/currency.ts';
import { DEFAULT_DECIMAL_NUMBER_COUNT } from '@/consts/numeral.ts';
import { TRANSACTION_MIN_AMOUNT, TRANSACTION_MAX_AMOUNT } from '@/consts/transaction.ts';
import { isNumber, replaceAll, removeAll } from '@/lib/common.ts';
import { isNumber, replaceAll } from '@/lib/common.ts';
import { evaluateExpression } from '@/lib/evaluator.ts';
import type { ComponentDensity } from '@/lib/ui/desktop.ts';
import logger from '@/lib/logger.ts';
@@ -112,11 +112,11 @@ const emit = defineEmits<{
const {
tt,
getCurrentNumeralSystemType,
getCurrentDecimalSeparator,
getCurrentDigitGroupingSymbol,
parseAmount,
formatAmount,
formatNumber,
parseAmountFromLocalizedNumerals,
formatAmountToLocalizedNumeralsWithoutDigitGrouping,
formatNumberToLocalizedNumerals,
getAmountPrependAndAppendText
} = useI18n();
@@ -124,7 +124,7 @@ const {
currentValue,
onKeyUpDown,
onPaste
} = useCommonNumberInputBase(props, DEFAULT_DECIMAL_NUMBER_COUNT, getInitedFormattedValue(props.modelValue, props.flipNegative), parseAmount, getFormattedValue, getValidFormattedValue);
} = useCommonNumberInputBase(props, DEFAULT_DECIMAL_NUMBER_COUNT, getInitedFormattedValue(props.modelValue, props.flipNegative), parseAmountFromLocalizedNumerals, getFormattedValue, getValidFormattedValue);
const snackbar = useTemplateRef<SnackBarType>('snackbar');
@@ -135,7 +135,7 @@ const rules = [
}
try {
const val = parseAmount(v);
const val = parseAmountFromLocalizedNumerals(v);
if (Number.isNaN(val) || !Number.isFinite(val)) {
return tt('Amount value is not number');
@@ -205,6 +205,7 @@ function enterFormulaMode(): void {
function calculateFormula(): void {
const systemDecimalSeparator = DecimalSeparator.Dot.symbol;
const numeralSystem = getCurrentNumeralSystemType();
const decimalSeparator = getCurrentDecimalSeparator();
let finalFormula = currentFormula.value;
@@ -215,12 +216,13 @@ function calculateFormula(): void {
finalFormula = replaceAll(currentFormula.value, decimalSeparator, systemDecimalSeparator);
}
finalFormula = numeralSystem.replaceLocalizedDigitsToWesternArabicDigits(finalFormula);
const calculatedValue = evaluateExpression(finalFormula);
if (isNumber(calculatedValue)) {
const textualValue = formatNumber(calculatedValue, 2);
const textualValue = formatNumberToLocalizedNumerals(calculatedValue, 2);
const hasDecimalSeparator = textualValue.indexOf(decimalSeparator) >= 0;
currentValue.value = getValidFormattedValue(calculatedValue, textualValue, hasDecimalSeparator);
currentValue.value = getValidFormattedValue(calculatedValue * 100, textualValue, hasDecimalSeparator);
formulaMode.value = false;
} else {
snackbar.value?.showMessage('Formula is invalid');
@@ -273,33 +275,36 @@ function getInitedFormattedValue(value: number, flipNegative?: boolean): string
}
function getFormattedValue(value: number): string {
const numeralSystem = getCurrentNumeralSystemType();
if (!Number.isNaN(value) && Number.isFinite(value)) {
const digitGroupingSymbol = getCurrentDigitGroupingSymbol();
return removeAll(formatAmount(value, props.currency), digitGroupingSymbol);
return formatAmountToLocalizedNumeralsWithoutDigitGrouping(value, props.currency);
}
return '0';
return numeralSystem.digitZero;
}
function getDisplayCurrencyPrependAndAppendText(): CurrencyPrependAndAppendText | null {
const numericCurrentValue = parseAmount(currentValue.value);
const numericCurrentValue = parseAmountFromLocalizedNumerals(currentValue.value);
const isPlural = numericCurrentValue !== 100 && numericCurrentValue !== -100;
return getAmountPrependAndAppendText(props.currency, isPlural);
}
watch(() => props.currency, () => {
const numeralSystem = getCurrentNumeralSystemType();
const newStringValue = getInitedFormattedValue(props.modelValue, props.flipNegative);
if (!(newStringValue === '0' && currentValue.value === '')) {
if (!(newStringValue === numeralSystem.digitZero && currentValue.value === '')) {
currentValue.value = newStringValue;
}
});
watch(() => props.flipNegative, (newValue) => {
const numeralSystem = getCurrentNumeralSystemType();
const newStringValue = getInitedFormattedValue(props.modelValue, newValue);
if (!(newStringValue === '0' && currentValue.value === '')) {
if (!(newStringValue === numeralSystem.digitZero && currentValue.value === '')) {
currentValue.value = newStringValue;
}
});
@@ -309,36 +314,55 @@ watch(() => props.modelValue, (newValue) => {
newValue = -newValue;
}
const numericCurrentValue = parseAmount(currentValue.value);
const numeralSystem = getCurrentNumeralSystemType();
const numericCurrentValue = parseAmountFromLocalizedNumerals(currentValue.value);
if (newValue !== numericCurrentValue) {
const newStringValue = getFormattedValue(newValue);
if (!(newStringValue === '0' && currentValue.value === '')) {
if (!(newStringValue === numeralSystem.digitZero && currentValue.value === '')) {
currentValue.value = newStringValue;
}
}
});
watch(currentValue, (newValue) => {
const numeralSystem = getCurrentNumeralSystemType();
let actualNumeralSystem: NumeralSystem | undefined = undefined;
let finalValue = '';
if (newValue) {
const decimalSeparator = getCurrentDecimalSeparator();
for (let i = 0; i < newValue.length; i++) {
if (!('0' <= newValue[i] && newValue[i] <= '9') && newValue[i] !== '-' && newValue[i] !== decimalSeparator) {
break;
if (newValue[0] === '-' || newValue[0] === decimalSeparator) {
actualNumeralSystem = NumeralSystem.detect(newValue[1]);
} else {
actualNumeralSystem = NumeralSystem.detect(newValue[0]);
}
if (actualNumeralSystem && (actualNumeralSystem.type === NumeralSystem.WesternArabicNumerals.type || actualNumeralSystem.type === numeralSystem.type)) {
for (let i = 0; i < newValue.length; i++) {
if (!NumeralSystem.WesternArabicNumerals.isDigit(newValue[i]) && !numeralSystem.isDigit(newValue[i]) && newValue[i] !== '-' && newValue[i] !== decimalSeparator) {
break;
}
finalValue += newValue[i];
}
finalValue += newValue[i];
finalValue = numeralSystem.replaceWesternArabicDigitsToLocalizedDigits(finalValue);
} else if (newValue === '-' || newValue === decimalSeparator || newValue === `-${decimalSeparator}`) {
finalValue = newValue;
}
}
if (finalValue !== newValue) {
currentValue.value = finalValue;
} else {
let value: number = parseAmount(finalValue);
let value: number = parseAmountFromLocalizedNumerals(finalValue);
if (Number.isNaN(value) || !Number.isFinite(value)) {
value = 0;
}
if (props.flipNegative) {
value = -value;
+17 -11
View File
@@ -25,9 +25,6 @@ import {
isArray,
isNumber
} from '@/lib/common.ts';
import {
formatAmount
} from '@/lib/numeral.ts';
import {
getYearMonthFirstUnixTime,
getYearMonthLastUnixTime,
@@ -73,7 +70,16 @@ const emit = defineEmits<{
}>();
const theme = useTheme();
const { tt, formatUnixTimeToShortYear, formatYearQuarter, formatUnixTimeToShortYearMonth, formatUnixTimeToFiscalYear, formatAmountWithCurrency } = useI18n();
const { tt,
formatUnixTimeToShortYear,
formatYearQuarter,
formatUnixTimeToShortYearMonth,
formatUnixTimeToFiscalYear,
formatAmountToWesternArabicNumeralsWithoutDigitGrouping,
formatAmountToLocalizedNumeralsWithCurrency
} = useI18n();
const { allDateRanges, getItemName, getColor } = useMonthlyTrendsChartBase(props);
const userStore = useUserStore();
@@ -252,8 +258,8 @@ const yAxisWidth = computed<number>(() => {
}
}
const maxValueText = formatAmountWithCurrency(maxValue, props.defaultCurrency);
const minValueText = formatAmountWithCurrency(minValue, props.defaultCurrency);
const maxValueText = formatAmountToLocalizedNumeralsWithCurrency(maxValue, props.defaultCurrency);
const minValueText = formatAmountToLocalizedNumeralsWithCurrency(minValue, props.defaultCurrency);
const maxLengthText = maxValueText.length > minValueText.length ? maxValueText : minValueText;
const canvas = document.createElement('canvas');
@@ -319,7 +325,7 @@ const chartOptions = computed<object>(() => {
const item = displayItems[i];
if (displayItems.length === 1 || item.totalAmount !== 0) {
const value = formatAmountWithCurrency(item.totalAmount, props.defaultCurrency);
const value = formatAmountToLocalizedNumeralsWithCurrency(item.totalAmount, props.defaultCurrency);
tooltip += '<div><span class="chart-pointer" style="background-color: ' + item.color + '"></span>';
tooltip += `<span>${item.name}</span><span style="margin-left: 20px; float: right">${value}</span><br/>`;
tooltip += '</div>';
@@ -327,7 +333,7 @@ const chartOptions = computed<object>(() => {
}
if (props.showTotalAmountInTooltip) {
const displayTotalAmount = formatAmountWithCurrency(totalAmount, props.defaultCurrency);
const displayTotalAmount = formatAmountToLocalizedNumeralsWithCurrency(totalAmount, props.defaultCurrency);
tooltip = '<div style="border-bottom: ' + (isDarkMode.value ? '#eee' : '#333') + ' dashed 1px">'
+ '<span class="chart-pointer" style="background-color: ' + (isDarkMode.value ? '#eee' : '#333') + '"></span>'
+ `<span>${tt('Total Amount')}</span><span style="margin-left: 20px; float: right">${displayTotalAmount}</span><br/>`
@@ -367,13 +373,13 @@ const chartOptions = computed<object>(() => {
type: 'value',
axisLabel: {
formatter: (value: string) => {
return formatAmountWithCurrency(value, props.defaultCurrency);
return formatAmountToLocalizedNumeralsWithCurrency(parseInt(value), props.defaultCurrency);
}
},
axisPointer: {
label: {
formatter: (params: CallbackDataParams) => {
return formatAmountWithCurrency(Math.floor(params.value as number), props.defaultCurrency);
return formatAmountToLocalizedNumeralsWithCurrency(Math.floor(params.value as number), props.defaultCurrency);
}
}
},
@@ -443,7 +449,7 @@ function exportData(): { headers: string[], data: string[][] } {
for (let i = 0; i < allDisplayDateRanges.value.length; i++) {
const row: string[] = [];
row.push(allDisplayDateRanges.value[i]);
row.push(...allSeries.value.map(item => formatAmount(item.data[i], {})));
row.push(...allSeries.value.map(item => formatAmountToWesternArabicNumeralsWithoutDigitGrouping(item.data[i])));
data.push(row);
}
+2 -2
View File
@@ -32,7 +32,7 @@ const emit = defineEmits<{
const theme = useTheme();
const { formatAmountWithCurrency } = useI18n();
const { formatAmountToLocalizedNumeralsWithCurrency } = useI18n();
const { selectedIndex, validItems } = usePieChartBase(props);
const selectedLegends = ref<Record<string, boolean> | null>(null);
@@ -133,7 +133,7 @@ const chartOptions = computed<object>(() => {
formatter: (params: CallbackDataParams) => {
const dataItem = params.data as DesktopPieChartDataItem;
const name = dataItem ? dataItem.displayName : '';
const value = dataItem ? dataItem.displayValue : formatAmountWithCurrency(params.value as number);
const value = dataItem ? dataItem.displayValue : formatAmountToLocalizedNumeralsWithCurrency(params.value as number);
let percent = dataItem ? dataItem.displayPercent : (params.percent + '%');
if (hasUnselectedItem.value) {