Initial scaffold for AvtoAmbor parts inventory
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>
This commit is contained in:
87
src/lib/i18n/store.js
Normal file
87
src/lib/i18n/store.js
Normal file
@ -0,0 +1,87 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user