Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 80 additions & 72 deletions alchemy_calc.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions alchemy_constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,13 @@ function getSmartLabel(currentRate, maxSpeed) {
const percent = (ratio * 100).toFixed(1) + "%";

if (match) {
const fracStr = (match.n === 1 && match.d === 1) ? "Full Belt" : `${match.n}/${match.d} Belt`;
const fracStr = (match.n === 1 && match.d === 1) ? I18n.t("Full Belt") : I18n.t("{n}/{d} Belt", {n: match.n, d: match.d});
const isApprox = Math.abs((match.n / match.d) - ratio) > 0.000001;
const prefix = isApprox ? "~" : "";
return `${prefix}${fracStr}, ${percent}`;
}

return `${percent} Load`;
return I18n.t("{percent} Load", {percent: percent});
}

// --- ITEM ID REGISTRY (Auto-Generated 2026-01-10) ---
Expand Down
106 changes: 106 additions & 0 deletions alchemy_i18n.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/**
* I18n - Lightweight translation module for Alchemy Factory Calculator.
* English keys are used as-is; other languages load from lang/{lang}.js.
* Uses script tag loading to avoid CORS issues with file:// protocol.
*/
const I18n = (() => {
let _lang = 'en';
let _dict = {};

/**
* Translate a key, with optional parameter substitution.
* @param {string} key - English source string (used as key)
* @param {Object} [params] - Placeholder values, e.g. {item: "Gold"}
* @returns {string}
*/
function t(key, params) {
let str = (_lang === 'en' || !_dict[key]) ? key : _dict[key];
if (params) {
for (const [k, v] of Object.entries(params)) {
str = str.replaceAll(`{${k}}`, v);
}
}
return str;
}

/** Apply translations to all elements with data-i18n attributes. */
function applyStaticTranslations() {
document.querySelectorAll('[data-i18n]').forEach(el => {
el.textContent = t(el.getAttribute('data-i18n'));
});
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
el.placeholder = t(el.getAttribute('data-i18n-placeholder'));
});
document.querySelectorAll('[data-i18n-title]').forEach(el => {
el.title = t(el.getAttribute('data-i18n-title'));
});
}

/**
* Initialize: read saved language from localStorage, load dictionary if needed.
*/
async function init() {
_lang = localStorage.getItem('alchemy-lang') || 'en';
if (_lang !== 'en') {
await _loadDict(_lang);
}
const sel = document.getElementById('lang-select');
if (sel) {
sel.value = _lang;
sel.addEventListener('change', (e) => setLang(e.target.value));
}
applyStaticTranslations();
}

/**
* Switch language, reload dictionary, re-render UI.
*/
async function setLang(lang) {
_lang = lang;
localStorage.setItem('alchemy-lang', lang);
_dict = {};
if (lang !== 'en') {
await _loadDict(lang);
}
applyStaticTranslations();
if (typeof renderAll === 'function') {
renderAll();
}
}

/**
* Load dictionary via script tag (works with file:// protocol).
*/
function _loadDict(lang) {
return new Promise((resolve) => {
// Check if already loaded via global variable
const globalVar = `LANG_${lang.toUpperCase()}`;
if (window[globalVar]) {
_dict = window[globalVar];
resolve();
return;
}

// Load via script tag
const script = document.createElement('script');
script.src = `lang/${lang}.js`;
script.onload = () => {
if (window[globalVar]) {
_dict = window[globalVar];
}
resolve();
};
script.onerror = () => {
console.warn(`I18n: Failed to load lang/${lang}.js`);
resolve();
};
document.head.appendChild(script);
});
}

function getLang() {
return _lang;
}

return { t, init, setLang, getLang, applyStaticTranslations };
})();
Loading