346 lines
7.7 KiB
TypeScript
346 lines
7.7 KiB
TypeScript
/**
|
||
* Enhetsvisning och -tolkning i appen (i18n-spec §9, §11).
|
||
* Canonical koder (GRAM, TABLESPOON …) kommer från API:t; här översätts de till
|
||
* lokala förkortningar och tolkas tillbaka från användarens fritext.
|
||
* Måttsystem-konvertering (M5) sker via displayQuantity i shared-types.
|
||
* DB-tabellen unit_translations är källan för fler språk (API: /v1/i18n/units).
|
||
*/
|
||
import { displayQuantity, type MeasurementSystem, type Unit } from "@app/shared-types";
|
||
|
||
const LABELS: Record<string, Record<string, string>> = {
|
||
sv: {
|
||
GRAM: "g",
|
||
KILOGRAM: "kg",
|
||
MILLILITER: "ml",
|
||
DECILITER: "dl",
|
||
LITER: "l",
|
||
TEASPOON: "tsk",
|
||
TABLESPOON: "msk",
|
||
CUP_US: "cup",
|
||
FLUID_OUNCE_US: "fl oz",
|
||
OUNCE: "oz",
|
||
POUND: "lb",
|
||
COUNT: "st",
|
||
PORTION: "portion",
|
||
PINCH: "krm",
|
||
SLICE: "skiva",
|
||
CLOVE: "klyfta",
|
||
CAN: "burk",
|
||
PACKAGE: "paket",
|
||
},
|
||
en: {
|
||
GRAM: "g",
|
||
KILOGRAM: "kg",
|
||
MILLILITER: "ml",
|
||
DECILITER: "dl",
|
||
LITER: "l",
|
||
TEASPOON: "tsp",
|
||
TABLESPOON: "tbsp",
|
||
CUP_US: "cup",
|
||
FLUID_OUNCE_US: "fl oz",
|
||
OUNCE: "oz",
|
||
POUND: "lb",
|
||
COUNT: "pcs",
|
||
PORTION: "serving",
|
||
PINCH: "pinch",
|
||
SLICE: "slice",
|
||
CLOVE: "clove",
|
||
CAN: "can",
|
||
PACKAGE: "pack",
|
||
},
|
||
es: {
|
||
GRAM: "g",
|
||
KILOGRAM: "kg",
|
||
MILLILITER: "ml",
|
||
DECILITER: "dl",
|
||
LITER: "l",
|
||
TEASPOON: "cdta",
|
||
TABLESPOON: "cda",
|
||
CUP_US: "taza",
|
||
FLUID_OUNCE_US: "fl oz",
|
||
OUNCE: "oz",
|
||
POUND: "lb",
|
||
COUNT: "ud",
|
||
PORTION: "ración",
|
||
PINCH: "pizca",
|
||
SLICE: "rebanada",
|
||
CLOVE: "diente",
|
||
CAN: "lata",
|
||
PACKAGE: "paquete",
|
||
},
|
||
it: {
|
||
GRAM: "g",
|
||
KILOGRAM: "kg",
|
||
MILLILITER: "ml",
|
||
DECILITER: "dl",
|
||
LITER: "l",
|
||
TEASPOON: "cucchiaino",
|
||
TABLESPOON: "cucchiaio",
|
||
CUP_US: "tazza",
|
||
FLUID_OUNCE_US: "fl oz",
|
||
OUNCE: "oz",
|
||
POUND: "lb",
|
||
COUNT: "pz",
|
||
PORTION: "porzione",
|
||
PINCH: "pizzico",
|
||
SLICE: "fetta",
|
||
CLOVE: "spicchio",
|
||
CAN: "lattina",
|
||
PACKAGE: "confezione",
|
||
},
|
||
de: {
|
||
GRAM: "g",
|
||
KILOGRAM: "kg",
|
||
MILLILITER: "ml",
|
||
DECILITER: "dl",
|
||
LITER: "l",
|
||
TEASPOON: "TL",
|
||
TABLESPOON: "EL",
|
||
CUP_US: "Cup",
|
||
FLUID_OUNCE_US: "fl oz",
|
||
OUNCE: "oz",
|
||
POUND: "lb",
|
||
COUNT: "Stk",
|
||
PORTION: "Portion",
|
||
PINCH: "Prise",
|
||
SLICE: "Scheibe",
|
||
CLOVE: "Zehe",
|
||
CAN: "Dose",
|
||
PACKAGE: "Packung",
|
||
},
|
||
fr: {
|
||
GRAM: "g",
|
||
KILOGRAM: "kg",
|
||
MILLILITER: "ml",
|
||
DECILITER: "dl",
|
||
LITER: "l",
|
||
TEASPOON: "c. à c.",
|
||
TABLESPOON: "c. à s.",
|
||
CUP_US: "cup",
|
||
FLUID_OUNCE_US: "fl oz",
|
||
OUNCE: "oz",
|
||
POUND: "lb",
|
||
COUNT: "pcs",
|
||
PORTION: "portion",
|
||
PINCH: "pincée",
|
||
SLICE: "tranche",
|
||
CLOVE: "gousse",
|
||
CAN: "boîte",
|
||
PACKAGE: "paquet",
|
||
},
|
||
da: {
|
||
GRAM: "g",
|
||
KILOGRAM: "kg",
|
||
MILLILITER: "ml",
|
||
DECILITER: "dl",
|
||
LITER: "l",
|
||
TEASPOON: "tsk",
|
||
TABLESPOON: "spsk",
|
||
CUP_US: "cup",
|
||
FLUID_OUNCE_US: "fl oz",
|
||
OUNCE: "oz",
|
||
POUND: "lb",
|
||
COUNT: "stk",
|
||
PORTION: "portion",
|
||
PINCH: "knsp",
|
||
SLICE: "skive",
|
||
CLOVE: "fed",
|
||
CAN: "dåse",
|
||
PACKAGE: "pakke",
|
||
},
|
||
nb: {
|
||
GRAM: "g",
|
||
KILOGRAM: "kg",
|
||
MILLILITER: "ml",
|
||
DECILITER: "dl",
|
||
LITER: "l",
|
||
TEASPOON: "ts",
|
||
TABLESPOON: "ss",
|
||
CUP_US: "cup",
|
||
FLUID_OUNCE_US: "fl oz",
|
||
OUNCE: "oz",
|
||
POUND: "lb",
|
||
COUNT: "stk",
|
||
PORTION: "porsjon",
|
||
PINCH: "knivsodd",
|
||
SLICE: "skive",
|
||
CLOVE: "båt",
|
||
CAN: "boks",
|
||
PACKAGE: "pakke",
|
||
},
|
||
fi: {
|
||
GRAM: "g",
|
||
KILOGRAM: "kg",
|
||
MILLILITER: "ml",
|
||
DECILITER: "dl",
|
||
LITER: "l",
|
||
TEASPOON: "tl",
|
||
TABLESPOON: "rkl",
|
||
CUP_US: "cup",
|
||
FLUID_OUNCE_US: "fl oz",
|
||
OUNCE: "oz",
|
||
POUND: "lb",
|
||
COUNT: "kpl",
|
||
PORTION: "annos",
|
||
PINCH: "hyppysellinen",
|
||
SLICE: "viipale",
|
||
CLOVE: "kynsi",
|
||
CAN: "tölkki",
|
||
PACKAGE: "paketti",
|
||
},
|
||
nl: {
|
||
GRAM: "g",
|
||
KILOGRAM: "kg",
|
||
MILLILITER: "ml",
|
||
DECILITER: "dl",
|
||
LITER: "l",
|
||
TEASPOON: "tl",
|
||
TABLESPOON: "el",
|
||
CUP_US: "cup",
|
||
FLUID_OUNCE_US: "fl oz",
|
||
OUNCE: "oz",
|
||
POUND: "lb",
|
||
COUNT: "st",
|
||
PORTION: "portie",
|
||
PINCH: "snufje",
|
||
SLICE: "plak",
|
||
CLOVE: "teentje",
|
||
CAN: "blik",
|
||
PACKAGE: "pak",
|
||
},
|
||
pl: {
|
||
GRAM: "g",
|
||
KILOGRAM: "kg",
|
||
MILLILITER: "ml",
|
||
DECILITER: "dl",
|
||
LITER: "l",
|
||
TEASPOON: "łyżeczka",
|
||
TABLESPOON: "łyżka",
|
||
CUP_US: "cup",
|
||
FLUID_OUNCE_US: "fl oz",
|
||
OUNCE: "oz",
|
||
POUND: "lb",
|
||
COUNT: "szt.",
|
||
PORTION: "porcja",
|
||
PINCH: "szczypta",
|
||
SLICE: "plaster",
|
||
CLOVE: "ząbek",
|
||
CAN: "puszka",
|
||
PACKAGE: "opakowanie",
|
||
},
|
||
pt: {
|
||
GRAM: "g",
|
||
KILOGRAM: "kg",
|
||
MILLILITER: "ml",
|
||
DECILITER: "dl",
|
||
LITER: "l",
|
||
TEASPOON: "c. chá",
|
||
TABLESPOON: "c. sopa",
|
||
CUP_US: "cup",
|
||
FLUID_OUNCE_US: "fl oz",
|
||
OUNCE: "oz",
|
||
POUND: "lb",
|
||
COUNT: "un",
|
||
PORTION: "dose",
|
||
PINCH: "pitada",
|
||
SLICE: "fatia",
|
||
CLOVE: "dente",
|
||
CAN: "lata",
|
||
PACKAGE: "embalagem",
|
||
},
|
||
};
|
||
|
||
let currentLanguage = "sv";
|
||
export function setUnitLanguage(language: string): void {
|
||
currentLanguage = LABELS[language] ? language : "sv";
|
||
}
|
||
|
||
/** Kod → lokal etikett ("GRAM" → "g", "TABLESPOON" → "msk"). */
|
||
export function unitLabel(unitCode: string): string {
|
||
return LABELS[currentLanguage]?.[unitCode] ?? LABELS.sv?.[unitCode] ?? unitCode.toLowerCase();
|
||
}
|
||
|
||
/** Fritext → kod ("msk" → TABLESPOON, "tbsp" → TABLESPOON, "st" → COUNT …). */
|
||
const PARSE: Record<string, string> = {
|
||
g: "GRAM",
|
||
gram: "GRAM",
|
||
grams: "GRAM",
|
||
kg: "KILOGRAM",
|
||
kilo: "KILOGRAM",
|
||
ml: "MILLILITER",
|
||
milliliter: "MILLILITER",
|
||
dl: "DECILITER",
|
||
deciliter: "DECILITER",
|
||
l: "LITER",
|
||
liter: "LITER",
|
||
litre: "LITER",
|
||
tsk: "TEASPOON",
|
||
tsp: "TEASPOON",
|
||
teaspoon: "TEASPOON",
|
||
msk: "TABLESPOON",
|
||
tbsp: "TABLESPOON",
|
||
tablespoon: "TABLESPOON",
|
||
krm: "MILLILITER", // 1 krm = 1 ml (dokumenterad standard)
|
||
cup: "CUP_US",
|
||
cups: "CUP_US",
|
||
"fl oz": "FLUID_OUNCE_US",
|
||
floz: "FLUID_OUNCE_US",
|
||
oz: "OUNCE",
|
||
ounce: "OUNCE",
|
||
lb: "POUND",
|
||
lbs: "POUND",
|
||
pound: "POUND",
|
||
st: "COUNT",
|
||
styck: "COUNT",
|
||
pcs: "COUNT",
|
||
pc: "COUNT",
|
||
stk: "COUNT",
|
||
portion: "PORTION",
|
||
portioner: "PORTION",
|
||
serving: "PORTION",
|
||
nypa: "PINCH",
|
||
pinch: "PINCH",
|
||
skiva: "SLICE",
|
||
skivor: "SLICE",
|
||
slice: "SLICE",
|
||
klyfta: "CLOVE",
|
||
klyftor: "CLOVE",
|
||
clove: "CLOVE",
|
||
burk: "CAN",
|
||
can: "CAN",
|
||
paket: "PACKAGE",
|
||
pkt: "PACKAGE",
|
||
pack: "PACKAGE",
|
||
package: "PACKAGE",
|
||
};
|
||
|
||
export function parseUnitInput(text: string): string | null {
|
||
const key = text.trim().toLowerCase();
|
||
if (!key) return null;
|
||
if (LABELS.sv?.[text.trim().toUpperCase()]) return text.trim().toUpperCase();
|
||
return PARSE[key] ?? null;
|
||
}
|
||
|
||
/** Användarens måttsystem (i18n M5) – sätts från locale-preferenserna. */
|
||
let currentSystem: MeasurementSystem = "METRIC";
|
||
export function setMeasurementSystem(system: MeasurementSystem): void {
|
||
currentSystem = system;
|
||
}
|
||
|
||
/**
|
||
* Kanonisk mängd -> visningssträng i användarens måttsystem (i18n-spec §9–10).
|
||
* Lagringen är alltid kanonisk; endast visningen konverterar. "≈" markerar
|
||
* konverterade (approximativa) värden – ärlighet före skenprecision.
|
||
*/
|
||
export function formatQuantity(quantity: number, unitCode: string): string {
|
||
const display = KNOWN_UNITS.has(unitCode)
|
||
? displayQuantity(quantity, unitCode as Unit, currentSystem)
|
||
: { value: quantity, unit: unitCode, approximate: false };
|
||
const v = display.value;
|
||
const rounded = Number.isInteger(v) ? String(v) : String(Math.round(v * 100) / 100);
|
||
const prefix = display.approximate ? "≈" : "";
|
||
return `${prefix}${rounded} ${unitLabel(String(display.unit))}`;
|
||
}
|
||
|
||
const KNOWN_UNITS = new Set(Object.keys(LABELS.sv ?? {}));
|