Add EUR currency support with multi-language translations

- Created EUR calculation module (calculate-eur.js) for EUR-based satoshi calculations
- Created btcpoll-eur.js to fetch and store EUR historical data from CoinGecko API
- Added EUR historical data file structure (public/eur_historical)
- Created locale files for 8 European languages (English, German, French, Spanish, Italian, Dutch, Portuguese, Polish)
- Updated index.js with EUR routes for all language variants (/en-eur, /de, /fr, /es, /it, /nl, /pt, /pl)
- Made template (sats.hbs) dynamic to support both HKD and EUR currencies with configurable exchange rates
- Updated template to support up to 7 language links for EUR version
- Added updaterate-eur.js for GitHub Actions to update EUR data daily
- Updated GitHub Actions workflow to run both HKD and EUR data updates
- Updated all locale files (HKD and EUR) with data_file, rate_field, and exchange_rate parameters

This implementation allows the site to work with EUR currency and supports translations
to all major European languages, preparing for deployment at eursat.eu domain.
This commit is contained in:
Claude
2025-11-09 09:09:13 +00:00
parent 0fb43ad77c
commit 292eebd9b3
18 changed files with 521 additions and 6 deletions

View File

@@ -21,9 +21,10 @@ jobs:
- name: add and push
run: |
npm install
npm install
npm install axios@0.26.1 --save
node updaterate.js
node updaterate-eur.js
date > generated.txt
git config user.name github-actions
git config user.email github-actions@github.com

80
btcpoll-eur.js Normal file
View File

@@ -0,0 +1,80 @@
const fs = require('fs')
const axios = require('axios');
const moment = require('moment');
const path = require('path');
const dirPath = path.join(__dirname, ".");
const fileToWrite = dirPath + "/public/eur_historical"
const fileToRead = dirPath + "/public/eur_historical"
// get btc/usd and btc/eur daily rate
async function BTCDaily() {
let url = "https://api.coingecko.com/api/v3/coins/bitcoin/history?localization=false&date="
const yesterday = moment().subtract(1, 'days') // YYYY-MM-DD
const reverse = yesterday.format('DD-MM-YYYY')
// format is YYYY-MM-DD
const dbdate = yesterday.format('YYYY-MM-DD')
let full_url = url + reverse
let row = {}
//console.log("db date: ", dbdate)
//console.log("new date format: ", reverse, "\n")
await axios.get(full_url).then(
async function(response) {
// console.log("full url: ", full_url)
const data = await response.data;
const btcusd = data['market_data']['current_price']['usd']
const btceur = data['market_data']['current_price']['eur']
const satsrate = 100000000
const sateur = parseInt(satsrate / btceur)
const usdsat = parseInt(satsrate / btcusd)
row = {
btcusd_rate: parseInt(btcusd),
date: dbdate,
usdsat_rate: usdsat,
sateur_rate: sateur,
btceur_rate: parseFloat(btceur).toFixed(2),
}
console.log("row data: ", row)
})
return row
}
// update file in the target github repo
async function updateFile() {
const row = await BTCDaily()
//const row = []
if (Object.keys(row).length > 0) {
//console.log("dirpath", dirPath)
//console.log("dirname", __dirname)
const original = await fs.readFileSync(fileToRead)
let orig = JSON.parse(original)
//console.log(orig[0])
orig.push(row)
//console.log(orig[orig.length - 1])
const new_content = JSON.stringify(orig)
await fs.writeFileSync(fileToWrite, new_content);
}
}
module.exports = {
// start here
main: async function () {
console.log("starting btcpoll script for sateur....")
let result = updateFile();
console.log(result)
return true
}
}
//const res = main()
//console.log('Result from main() : ', res)

58
calculate-eur.js Normal file
View File

@@ -0,0 +1,58 @@
const axios = require('axios')
const fs = require('fs');
module.exports = {
bfx: async function() {
const eurrate = 0.92 // approximate EUR/USD rate
const btcDataURL = "https://api-pub.bitfinex.com/v2/ticker/tBTCUSD"
const response = await axios.get(btcDataURL)
const data = response.data
//console.log(data[6])
const satDenominator = 100000000
// see docs : https://docs.bitfinex.com/reference#rest-public-ticker
btcLastPrice = data[6]
const sateur = Math.round((1 / btcLastPrice) * satDenominator * eurrate)
//console.log("bitfinex last price: ", btcLastPrice, "current satEUR: ", sateur)
return sateur
},
get10yr: async function() {
// console.log("get10yr")
try {
// const content = await fs.readFile('./public/eur_historical')
const content = fs.readFileSync('./public/eur_historical', { encoding: 'utf8' })
const historical = JSON.parse(content)
hist_entries = []
let datelist = []
// get all the years you need from 1 - 10
for (let i = 1; i < 11; i++) {
const y = new Date(new Date().setFullYear(new Date().getFullYear() - i)).toISOString().slice(0, 10)
datelist.push(y)
}
for (let j = 0; j < historical.length; j++) {
const hdate = historical[j]['date']
if (datelist.includes(hdate)) {
hist_entries.push(historical[j])
}
}
// console.log(hist_entries)
let final_list = []
let today_sats = await this.bfx()
for (var v = 0; v < hist_entries.length; v++) {
const date = new Date(hist_entries[v]['date'])
year = date.getFullYear();
rawsat = hist_entries[v]['sateur_rate']
percentage = (-100 * ((rawsat - today_sats) / rawsat)).toFixed(3)
final_list.push({ "year": date.toLocaleDateString(), "sats": rawsat.toLocaleString("en-US"), "percent": percentage });
}
return final_list.reverse()
} catch (error) {
console.error("Error trying to read file ", error)
}
}
}

View File

@@ -11,9 +11,18 @@ const handlebars = require('express-handlebars');
const port = 3000;
const calculate = require('./calculate')
const calculateEur = require('./calculate-eur')
const zhcnjson = require('./locales/zh-cn.json');
const zhhkjson = require('./locales/zh-hk.json');
const enjson = require('./locales/en.json');
const eneurjson = require('./locales/en-eur.json');
const dejson = require('./locales/de.json');
const frjson = require('./locales/fr.json');
const esjson = require('./locales/es.json');
const itjson = require('./locales/it.json');
const nljson = require('./locales/nl.json');
const ptjson = require('./locales/pt.json');
const pljson = require('./locales/pl.json');
app.set('view engine', 'hbs');
app.set('views', __dirname + '/views')
@@ -62,6 +71,71 @@ app.get('/zh-hk', function(req, res) {
})
});
// EUR routes
app.get('/en-eur', function(req, res) {
calculateEur.get10yr().then(pydata => {
const yeardata = { 'yeardata': pydata }
let eneurdata = Object.assign(eneurjson, yeardata)
res.render('sats', eneurdata)
})
});
app.get('/de', function(req, res) {
calculateEur.get10yr().then(pydata => {
const yeardata = { 'yeardata': pydata }
let dedata = Object.assign(dejson, yeardata)
res.render('sats', dedata)
})
});
app.get('/fr', function(req, res) {
calculateEur.get10yr().then(pydata => {
const yeardata = { 'yeardata': pydata }
let frdata = Object.assign(frjson, yeardata)
res.render('sats', frdata)
})
});
app.get('/es', function(req, res) {
calculateEur.get10yr().then(pydata => {
const yeardata = { 'yeardata': pydata }
let esdata = Object.assign(esjson, yeardata)
res.render('sats', esdata)
})
});
app.get('/it', function(req, res) {
calculateEur.get10yr().then(pydata => {
const yeardata = { 'yeardata': pydata }
let itdata = Object.assign(itjson, yeardata)
res.render('sats', itdata)
})
});
app.get('/nl', function(req, res) {
calculateEur.get10yr().then(pydata => {
const yeardata = { 'yeardata': pydata }
let nldata = Object.assign(nljson, yeardata)
res.render('sats', nldata)
})
});
app.get('/pt', function(req, res) {
calculateEur.get10yr().then(pydata => {
const yeardata = { 'yeardata': pydata }
let ptdata = Object.assign(ptjson, yeardata)
res.render('sats', ptdata)
})
});
app.get('/pl', function(req, res) {
calculateEur.get10yr().then(pydata => {
const yeardata = { 'yeardata': pydata }
let pldata = Object.assign(pljson, yeardata)
res.render('sats', pldata)
})
});
//Makes the app listen to port 3000
app.listen(port, () => console.log(`App listening to port ${port}`));

35
locales/de.json Normal file
View File

@@ -0,0 +1,35 @@
{
"post": {
"author": "Janith Kasun",
"image": "https://picsum.photos/500/500",
"comments": [
"This is the first comment",
"This is the second comment",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum nec fermentum ligula. Sed vitae erat lectus."
]
},
"layout": "main",
"Title": "1 Euro ist derzeit wert ",
"subtitle": "EURSAT Historische Performance",
"date": "Datum",
"price": "Preis",
"percentchange": "Prozentuale Änderung",
"footnote": "Datenquelle von usdsat.com, angepasst für EUR",
"data_file": "eur_historical",
"rate_field": "sateur_rate",
"exchange_rate": "0.92",
"lang1_link": "/en-eur/",
"lang1": "English",
"lang2_link": "/fr/",
"lang2": "Français",
"lang3_link": "/es/",
"lang3": "Español",
"lang4_link": "/it/",
"lang4": "Italiano",
"lang5_link": "/nl/",
"lang5": "Nederlands",
"lang6_link": "/pt/",
"lang6": "Português",
"lang7_link": "/pl/",
"lang7": "Polski"
}

35
locales/en-eur.json Normal file
View File

@@ -0,0 +1,35 @@
{
"post": {
"author": "Janith Kasun",
"image": "https://picsum.photos/500/500",
"comments": [
"This is the first comment",
"This is the second comment",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum nec fermentum ligula. Sed vitae erat lectus."
]
},
"layout": "main",
"Title": "1 Euro is currently worth ",
"subtitle": "EURSAT Historical Performance",
"date": "Date",
"price": "Price",
"percentchange": "Percent Change",
"footnote": "data source from usdsat.com, adapted for EUR",
"data_file": "eur_historical",
"rate_field": "sateur_rate",
"exchange_rate": "0.92",
"lang1_link": "/de/",
"lang1": "Deutsch",
"lang2_link": "/fr/",
"lang2": "Français",
"lang3_link": "/es/",
"lang3": "Español",
"lang4_link": "/it/",
"lang4": "Italiano",
"lang5_link": "/nl/",
"lang5": "Nederlands",
"lang6_link": "/pt/",
"lang6": "Português",
"lang7_link": "/pl/",
"lang7": "Polski"
}

View File

@@ -15,6 +15,9 @@
"price": "Price",
"percentchange": "Percent Change",
"footnote": "data source from usdsat.com, adapted for HKD",
"data_file": "hkd_historical",
"rate_field": "sathkd_rate",
"exchange_rate": "7.75",
"lang1_link": "/zh-cn/",
"lang1": "中文(中国)",
"lang2_link": "/zh-hk/",

35
locales/es.json Normal file
View File

@@ -0,0 +1,35 @@
{
"post": {
"author": "Janith Kasun",
"image": "https://picsum.photos/500/500",
"comments": [
"This is the first comment",
"This is the second comment",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum nec fermentum ligula. Sed vitae erat lectus."
]
},
"layout": "main",
"Title": "1 Euro vale actualmente ",
"subtitle": "Rendimiento Histórico EURSAT",
"date": "Fecha",
"price": "Precio",
"percentchange": "Cambio Porcentual",
"footnote": "fuente de datos de usdsat.com, adaptado para EUR",
"data_file": "eur_historical",
"rate_field": "sateur_rate",
"exchange_rate": "0.92",
"lang1_link": "/en-eur/",
"lang1": "English",
"lang2_link": "/de/",
"lang2": "Deutsch",
"lang3_link": "/fr/",
"lang3": "Français",
"lang4_link": "/it/",
"lang4": "Italiano",
"lang5_link": "/nl/",
"lang5": "Nederlands",
"lang6_link": "/pt/",
"lang6": "Português",
"lang7_link": "/pl/",
"lang7": "Polski"
}

35
locales/fr.json Normal file
View File

@@ -0,0 +1,35 @@
{
"post": {
"author": "Janith Kasun",
"image": "https://picsum.photos/500/500",
"comments": [
"This is the first comment",
"This is the second comment",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum nec fermentum ligula. Sed vitae erat lectus."
]
},
"layout": "main",
"Title": "1 Euro vaut actuellement ",
"subtitle": "Performance Historique EURSAT",
"date": "Date",
"price": "Prix",
"percentchange": "Variation en Pourcentage",
"footnote": "source de données de usdsat.com, adaptée pour EUR",
"data_file": "eur_historical",
"rate_field": "sateur_rate",
"exchange_rate": "0.92",
"lang1_link": "/en-eur/",
"lang1": "English",
"lang2_link": "/de/",
"lang2": "Deutsch",
"lang3_link": "/es/",
"lang3": "Español",
"lang4_link": "/it/",
"lang4": "Italiano",
"lang5_link": "/nl/",
"lang5": "Nederlands",
"lang6_link": "/pt/",
"lang6": "Português",
"lang7_link": "/pl/",
"lang7": "Polski"
}

35
locales/it.json Normal file
View File

@@ -0,0 +1,35 @@
{
"post": {
"author": "Janith Kasun",
"image": "https://picsum.photos/500/500",
"comments": [
"This is the first comment",
"This is the second comment",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum nec fermentum ligula. Sed vitae erat lectus."
]
},
"layout": "main",
"Title": "1 Euro vale attualmente ",
"subtitle": "Performance Storica EURSAT",
"date": "Data",
"price": "Prezzo",
"percentchange": "Variazione Percentuale",
"footnote": "fonte dati da usdsat.com, adattato per EUR",
"data_file": "eur_historical",
"rate_field": "sateur_rate",
"exchange_rate": "0.92",
"lang1_link": "/en-eur/",
"lang1": "English",
"lang2_link": "/de/",
"lang2": "Deutsch",
"lang3_link": "/fr/",
"lang3": "Français",
"lang4_link": "/es/",
"lang4": "Español",
"lang5_link": "/nl/",
"lang5": "Nederlands",
"lang6_link": "/pt/",
"lang6": "Português",
"lang7_link": "/pl/",
"lang7": "Polski"
}

35
locales/nl.json Normal file
View File

@@ -0,0 +1,35 @@
{
"post": {
"author": "Janith Kasun",
"image": "https://picsum.photos/500/500",
"comments": [
"This is the first comment",
"This is the second comment",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum nec fermentum ligula. Sed vitae erat lectus."
]
},
"layout": "main",
"Title": "1 Euro is momenteel waard ",
"subtitle": "EURSAT Historische Prestaties",
"date": "Datum",
"price": "Prijs",
"percentchange": "Procentuele Verandering",
"footnote": "gegevensbron van usdsat.com, aangepast voor EUR",
"data_file": "eur_historical",
"rate_field": "sateur_rate",
"exchange_rate": "0.92",
"lang1_link": "/en-eur/",
"lang1": "English",
"lang2_link": "/de/",
"lang2": "Deutsch",
"lang3_link": "/fr/",
"lang3": "Français",
"lang4_link": "/es/",
"lang4": "Español",
"lang5_link": "/it/",
"lang5": "Italiano",
"lang6_link": "/pt/",
"lang6": "Português",
"lang7_link": "/pl/",
"lang7": "Polski"
}

35
locales/pl.json Normal file
View File

@@ -0,0 +1,35 @@
{
"post": {
"author": "Janith Kasun",
"image": "https://picsum.photos/500/500",
"comments": [
"This is the first comment",
"This is the second comment",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum nec fermentum ligula. Sed vitae erat lectus."
]
},
"layout": "main",
"Title": "1 Euro jest obecnie warte ",
"subtitle": "Historyczna Wydajność EURSAT",
"date": "Data",
"price": "Cena",
"percentchange": "Zmiana Procentowa",
"footnote": "źródło danych z usdsat.com, dostosowane dla EUR",
"data_file": "eur_historical",
"rate_field": "sateur_rate",
"exchange_rate": "0.92",
"lang1_link": "/en-eur/",
"lang1": "English",
"lang2_link": "/de/",
"lang2": "Deutsch",
"lang3_link": "/fr/",
"lang3": "Français",
"lang4_link": "/es/",
"lang4": "Español",
"lang5_link": "/it/",
"lang5": "Italiano",
"lang6_link": "/nl/",
"lang6": "Nederlands",
"lang7_link": "/pt/",
"lang7": "Português"
}

35
locales/pt.json Normal file
View File

@@ -0,0 +1,35 @@
{
"post": {
"author": "Janith Kasun",
"image": "https://picsum.photos/500/500",
"comments": [
"This is the first comment",
"This is the second comment",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum nec fermentum ligula. Sed vitae erat lectus."
]
},
"layout": "main",
"Title": "1 Euro vale atualmente ",
"subtitle": "Desempenho Histórico EURSAT",
"date": "Data",
"price": "Preço",
"percentchange": "Variação Percentual",
"footnote": "fonte de dados de usdsat.com, adaptado para EUR",
"data_file": "eur_historical",
"rate_field": "sateur_rate",
"exchange_rate": "0.92",
"lang1_link": "/en-eur/",
"lang1": "English",
"lang2_link": "/de/",
"lang2": "Deutsch",
"lang3_link": "/fr/",
"lang3": "Français",
"lang4_link": "/es/",
"lang4": "Español",
"lang5_link": "/it/",
"lang5": "Italiano",
"lang6_link": "/nl/",
"lang6": "Nederlands",
"lang7_link": "/pl/",
"lang7": "Polski"
}

View File

@@ -5,6 +5,9 @@
"price": "价格",
"percentchange": "百分比变化",
"footnote": "来自usdsat.com的数据源",
"data_file": "hkd_historical",
"rate_field": "sathkd_rate",
"exchange_rate": "7.75",
"lang1_link": "/",
"lang1": "EN",
"lang2_link": "/zh-hk/",

View File

@@ -5,6 +5,9 @@
"price" : "價格",
"percentchange" : "百分比變化",
"footnote" : "來自usdsat.com的數據源",
"data_file": "hkd_historical",
"rate_field": "sathkd_rate",
"exchange_rate": "7.75",
"lang1_link": "/zh-cn/",
"lang1": "中文(中国)",
"lang2_link": "/",

1
public/eur_historical Normal file
View File

@@ -0,0 +1 @@
[]

13
updaterate-eur.js Normal file
View File

@@ -0,0 +1,13 @@
const core = require('@actions/core');
const poll = require('./btcpoll-eur');
// modify this to ping url, get data and update, commit and push file to github repo
try {
const res = poll.main()
console.log("main response: ", res)
core.setOutput('✅ Success');
} catch (error) {
core.setFailed(`🛑 ${error.message}`);
}

View File

@@ -58,8 +58,7 @@
</li>
</ul>
<span class="navbar-item" style="color: #fff">
<a class="px-2" href="{{ lang1_link }}" style="color: rgb(6, 168, 231)"> {{ lang1 }} </a> |
<a class="px-2" href="{{ lang2_link }}" style="color: rgb(6, 168, 231)"> {{ lang2 }}</a>
<a class="px-2" href="{{ lang1_link }}" style="color: rgb(6, 168, 231)"> {{ lang1 }} </a>{{#if lang2}} | <a class="px-2" href="{{ lang2_link }}" style="color: rgb(6, 168, 231)"> {{ lang2 }}</a>{{/if}}{{#if lang3}} | <a class="px-2" href="{{ lang3_link }}" style="color: rgb(6, 168, 231)"> {{ lang3 }}</a>{{/if}}{{#if lang4}} | <a class="px-2" href="{{ lang4_link }}" style="color: rgb(6, 168, 231)"> {{ lang4 }}</a>{{/if}}{{#if lang5}} | <a class="px-2" href="{{ lang5_link }}" style="color: rgb(6, 168, 231)"> {{ lang5 }}</a>{{/if}}{{#if lang6}} | <a class="px-2" href="{{ lang6_link }}" style="color: rgb(6, 168, 231)"> {{ lang6 }}</a>{{/if}}{{#if lang7}} | <a class="px-2" href="{{ lang7_link }}" style="color: rgb(6, 168, 231)"> {{ lang7 }}</a>{{/if}}
</span>
</div>
</div>
@@ -125,7 +124,7 @@
i.forEach(function(item) {
if (Array.isArray(item)) {
var btc_price = item[0];
currentPrice = Math.round((1 / btc_price) * 100000000 / 7.75); // satoshis per HKD dollar
currentPrice = Math.round((1 / btc_price) * 100000000 / {{ exchange_rate }}); // satoshis per currency unit
document.title = currentPrice.toLocaleString() + " sats";
document.querySelector('#current').textContent = currentPrice.toLocaleString();
@@ -165,7 +164,7 @@
</p>
</div>
<script>
d3.json('/hkd_historical', function(data) {
d3.json('/{{ data_file }}', function(data) {
data = MG.convert.date(data, 'date');
var windowWidth = $(window).width();
@@ -230,7 +229,7 @@
xax_count: 16,
yax_count: 12,
x_accessor: 'date',
y_accessor: 'sathkd_rate',
y_accessor: '{{ rate_field }}',
y_scale_type: 'log',
y_extended_ticks: true,
yax_units: ' sats',