I expected this one to be a boring calculator.
Add up car costs, subtract transit costs, maybe draw a chart, done. But once I read through the Vue component, the interesting part wasn't the arithmetic itself — it was all the small decisions around what counts as a car cost, how to present money in different locales, and what the tool chooses to do when the “ditch the car” scenario is actually worse.
That's the stuff I like in small utility sites. The honest details are where the real implementation lives.
It isn't just translated — the numeric scale changes by locale
The first thing that surprised me was that the component doesn't just swap labels between languages. It also changes the expected scale of the defaults, and for some locales it displays the car's resale value in units of ten-thousands instead of raw currency units.
const myriadLocales = ["tw", "cn", "jp", "kr"];
const isMyriad = computed(() => myriadLocales.includes(locale.value));
const defaults = isMyriad.value
? {
carLoanMonthly: 15000,
insuranceYearly: 24000,
// ...
carResaleValue: 300000,
}
: {
carLoanMonthly: 400,
insuranceYearly: 1500,
// ...
carResaleValue: 12000,
};
And then the resale-value field gets a getter/setter pair that converts between stored value and displayed value:
const carResaleDisplay = computed({
get: () =>
isMyriad.value
? Math.round((state.carResaleValue / 10000) * 100) / 100
: state.carResaleValue,
set: (val) => {
const num = Number(val) || 0;
state.carResaleValue = isMyriad.value
? Math.round(num * 10000)
: Math.round(num);
},
});
I like this a lot because it solves a real UX problem: a Taiwanese or Japanese user usually doesn't want to think about a resale value as 300000 in the input box if the mental model is “30 萬.” The code keeps one raw numeric value in state, but gives some locales a friendlier unit system at the input boundary.
That also tells you something about the tool's intent. It's not trying to be a pure spreadsheet clone. It's trying to feel locally natural.
The core comparison is deliberately boring in the best way
The actual “should I own a car?” math is refreshingly direct. Instead of inventing anything fancy, the component normalizes all yearly costs down to monthly values, then compares that against a separate bucket for car-free transport.
const carCostMonthly = computed(() => {
return (
(state.carLoanMonthly || 0) +
(state.insuranceYearly || 0) / 12 +
(state.fuelMonthly || 0) +
(state.maintenanceYearly || 0) / 12 +
(state.parkingMonthly || 0) +
(state.taxFeeYearly || 0) / 12 +
(state.depreciationYearly || 0) / 12
);
});
const altCostMonthly = computed(() => {
return (
(state.transitMonthly || 0) +
(state.rideshareMonthly || 0) +
(state.carRentalYearly || 0) / 12
);
});
const netMonthlySavings = computed(() => carCostMonthly.value - altCostMonthly.value);
const netYearlySavings = computed(() => netMonthlySavings.value * 12);
What makes this better than a lot of “save money without a car” widgets is that it doesn't pretend the alternative is free. Public transit, rideshare, and occasional rentals all get their own inputs. That's a small thing, but it's the difference between a moralizing calculator and a practical one.
The other good call is including depreciation right in the same formula as insurance, parking, and fuel. The template even gives it tooltip help because the author clearly knows that's the input people are most likely to skip. Technically it's optional, but conceptually the tool treats it like a first-class ownership cost, which is exactly right.
The investment model rolls forward year by year, and that choice matters
The second half of the tool takes the “money you didn't spend on the car” idea and turns it into an investment simulation. Instead of using one black-box formula, it walks forward year by year and records two lines: compounded value and simple contributions.
const calculateProjection = () => {
const r = state.annualReturnRate / 100;
const annualContribution = Math.max(0, netYearlySavings.value);
const years = state.investYears;
let growthValue = Math.max(0, state.carResaleValue || 0);
let contributionValue = growthValue;
let chartLabels = [];
let growthData = [];
let contributionData = [];
for (let i = 0; i <= years; i++) {
chartLabels.push(`${i}`);
growthData.push(Math.round(growthValue));
contributionData.push(Math.round(contributionValue));
growthValue = growthValue * (1 + r) + annualContribution;
contributionValue = contributionValue + annualContribution;
}
There are two implementation choices here that I think are worth calling out.
First, the resale value of the car becomes the starting principal. That's a nice touch because it reflects the actual decision path: if you sell the car, you don't just free up monthly cash flow, you may also unlock a lump sum on day one.
Second, the chart is comparing invested growth against raw contributions only, not against “keeping the car.” So the visualization isn't really an all-in personal-finance simulator; it's a focused answer to a narrower question: if I stop owning this car, what could that freed-up money become over time?
That's also why the chart code stays simple. It just feeds the two arrays into Chart.js and updates labels and datasets whenever the inputs change:
if (savingsChart) {
savingsChart.data.labels = chartLabels;
savingsChart.data.datasets[0].label = t("carFreeSavingsCalculator.growthLine");
savingsChart.data.datasets[0].data = growthData;
savingsChart.data.datasets[1].label = t("carFreeSavingsCalculator.contributionLine");
savingsChart.data.datasets[1].data = contributionData;
savingsChart.update();
}
It's not mathematically ambitious, but it is legible, and for this kind of tool that's usually the better trade.
The page is content-driven too, not just form-driven
One thing I didn't expect: a lot of the page's long-form content is also localized inside the language file, not hardcoded in the component. The intro block is rendered straight from translations:
<section
class="Intro"
v-html="t('carFreeSavingsCalculator.intro')"
></section>
And the metadata layer pulls from the same translation source:
const jsonld = JSON.stringify({
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: [
// ...
{
"@type": "ListItem",
position: 2,
item: {
"@id": t("carFreeSavingsCalculator.canonical"),
name: t("carFreeSavingsCalculator.h1"),
description: t("carFreeSavingsCalculator.description"),
},
},
],
});
let alternate = hreflangGenerator("car-free-savings-calculator");
So this isn't just “one calculator with translated button text.” Each locale has its own title, canonical, description, h1, h2, and a full intro/FAQ block in the language file. That explains why the component can generate a proper multilingual page instead of a thin wrapper around one English version.
I also think this is why the page feels more complete than a lot of little tools. It's half calculator, half localized explainer.
Honest limitations and gotchas
The main gotcha is in the projection logic: negative savings are shown to the user, but they are not modeled as negative annual contributions.
The component explicitly does this:
const annualContribution = Math.max(0, netYearlySavings.value);
// ...
interestEarned: Math.max(0, finalValue - totalContributions),
So if your transit/rideshare/rental plan costs more than keeping the car, the UI warns you, but the chart doesn't go below zero or show capital being drained. It just stops contributions at zero and, if you entered a resale value, continues compounding that lump sum. That's a reasonable product decision, but it's definitely more optimistic than a full cashflow model.
The localization is also only partial in the money-formatting layer. Inputs are formatted with a hard-coded comma regex, chart tooltips always prepend $, and the axis labels call toLocaleString() without tying it to the selected site locale. In other words: the content and labels are multilingual, but the currency presentation is still fairly generic.
There's one more subtle edge case: the locale-based defaults are chosen once at setup time from isMyriad.value. If someone switches languages after the component is already mounted, the labels and some display behavior update reactively, but the starting example numbers don't automatically reset to the new locale's typical scale. Not a disaster, just one of those little state-model details that shows up when localization affects numbers, not just text.
That mix of solid fundamentals and a few visible tradeoffs is exactly why I found the implementation interesting. I turned it into a small free tool: Car-Free Savings Calculator.
Available in other languages
- 不養車省錢計算器 — 繁體中文
- 不养车省钱计算器 — 简体中文
- Car-Free Savings Calculator — English
- 車を持たない節約シミュレーター — 日本語
- 차 없이 절약 계산기 — 한국어
- Calculateur d'économies sans voiture — Français
- Калькулятор экономии без машины — Русский
- Auto-frei Sparrechner — Deutsch
- Kalkulator Hemat Tanpa Mobil — Bahasa Indonesia
- Calculadora de Ahorro sin Coche — Español
- Máy Tính Tiết Kiệm Không Xe Hơi — Tiếng Việt
- เครื่องคำนวณเงินออมจากการไม่มีรถ — ไทย
- Kalkulator Oszczędności Bez Samochodu — Polski
- Arabasız Tasarruf Hesaplayıcı — Türkçe
- Calcolatore Risparmio Senza Auto — Italiano
- Calculadora de Economia Sem Carro — Português
- Autovrij Besparen Calculator — Nederlands
- Калькулятор економії без авто — Українська
Top comments (0)