SvelteKit 2 + Svelte 4 + adapter-node, SQLite via better-sqlite3 (WAL, foreign keys on). Bilingual EN/Тоҷикӣ throughout, locale persisted in localStorage. Pages: dashboard (totals, low stock, recent movements), parts list with search and sort, part create/edit, record movement (in/out/adjust with smart unit-price and adjust-quantity prefill), suppliers list with inline add. Schema: categories, suppliers, parts (with _en/_tg name+description columns, dirams for money), stock_movements with check on movement_type. On-hand updates are done in JS inside a transaction with the movement insert. Dockerized dev: docker compose, named project, bind-mounted data/ for DB persistence. Seed contains 6 categories, 4 suppliers, 31 realistic parts (Lada / Nexia / Opel / Toyota bias). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
88 lines
2.5 KiB
JavaScript
88 lines
2.5 KiB
JavaScript
import { writable, derived } from 'svelte/store';
|
|
import { browser } from '$app/environment';
|
|
import en from './en.json';
|
|
import tg from './tg.json';
|
|
|
|
const DICTS = { en, tg };
|
|
const STORAGE_KEY = 'avtoambor.locale';
|
|
const DEFAULT_LOCALE = 'tg';
|
|
|
|
// Warn at most once per missing key, so the console doesn't flood.
|
|
const _warned = new Set();
|
|
|
|
function lookup(dict, key) {
|
|
const parts = key.split('.');
|
|
let v = dict;
|
|
for (const p of parts) {
|
|
if (v == null) return undefined;
|
|
v = v[p];
|
|
}
|
|
return v;
|
|
}
|
|
|
|
export const locale = writable(
|
|
(browser && localStorage.getItem(STORAGE_KEY)) || DEFAULT_LOCALE
|
|
);
|
|
|
|
if (browser) {
|
|
locale.subscribe((value) => {
|
|
try { localStorage.setItem(STORAGE_KEY, value); } catch { /* ignore */ }
|
|
document.documentElement.setAttribute('lang', value);
|
|
});
|
|
}
|
|
|
|
export const t = derived(locale, ($locale) => {
|
|
return (key) => {
|
|
const primary = lookup(DICTS[$locale], key);
|
|
if (primary != null) return primary;
|
|
const fallback = lookup(DICTS.en, key);
|
|
if (fallback != null) {
|
|
if (!_warned.has(key)) {
|
|
_warned.add(key);
|
|
console.warn(`[i18n] missing "${key}" for locale "${$locale}"; using English.`);
|
|
}
|
|
return fallback;
|
|
}
|
|
if (!_warned.has(key)) {
|
|
_warned.add(key);
|
|
console.warn(`[i18n] missing key "${key}"`);
|
|
}
|
|
return key;
|
|
};
|
|
});
|
|
|
|
export function toggleLocale() {
|
|
locale.update((v) => (v === 'en' ? 'tg' : 'en'));
|
|
}
|
|
|
|
// Pick the right column from a record that has both _en and _tg fields,
|
|
// e.g. localized(part, 'name', $locale) → part.name_tg or part.name_en.
|
|
// Falls back to whichever language has content (not just English) so a
|
|
// TG-only entry still renders for an EN viewer.
|
|
export function localized(record, baseField, lang) {
|
|
if (!record) return '';
|
|
return (
|
|
record[`${baseField}_${lang}`] ||
|
|
record[`${baseField}_en`] ||
|
|
record[`${baseField}_tg`] ||
|
|
''
|
|
);
|
|
}
|
|
|
|
// True if the record has a non-empty value in the requested language.
|
|
// Used to flag "(missing translation)" when we had to fall back.
|
|
export function hasTranslation(record, baseField, lang) {
|
|
if (!record) return false;
|
|
const v = record[`${baseField}_${lang}`];
|
|
return v != null && String(v).trim() !== '';
|
|
}
|
|
|
|
// Money helpers: dirams ↔ display string.
|
|
export function formatMoney(dirams, lang = 'en') {
|
|
if (dirams == null) return '';
|
|
const n = Number(dirams) / 100;
|
|
// Tajik uses comma decimal separator in everyday use; English uses period.
|
|
const s = n.toFixed(2);
|
|
return lang === 'tg' ? s.replace('.', ',') : s;
|
|
}
|