i18n: fix placeholder mismatch, add validation (#23541)
This commit is contained in:
parent
d8ed7c1e11
commit
13b4ab3018
15 changed files with 269 additions and 51 deletions
|
|
@ -2,16 +2,16 @@
|
|||
"batterySettings": {
|
||||
"batteryLevel": "مستوى البطارية",
|
||||
"bufferStart": {
|
||||
"full": "عندما أوشكت على الامتلاء",
|
||||
"full": "عندما أوشكت على الامتلاء {soc}.",
|
||||
"never": "فقط مع فائض كافٍ"
|
||||
},
|
||||
"legendBottomName": "أولوية المنزل",
|
||||
"legendBottomSubline": "لا تستخدم للشحن",
|
||||
"legendBottomSubline": "حتى تصل إلى {soc}.",
|
||||
"legendMiddleName": "السيارة أولا",
|
||||
"legendMiddleSubline": "المنزل الثاني",
|
||||
"legendMiddleSubline": "عندما تكون بطارية المنزل فوق {soc}.",
|
||||
"legendTopAutostart": "يبدأ تلقائيًا",
|
||||
"legendTopName": "بطارية تدعم الشحن",
|
||||
"legendTopSubline": "بدون انقطاع",
|
||||
"legendTopSubline": "عندما تكون بطارية المنزل فوق {soc}.",
|
||||
"modalTitle": "إعدادات البطارية"
|
||||
},
|
||||
"config": {
|
||||
|
|
|
|||
12
i18n/ca.json
12
i18n/ca.json
|
|
@ -2,19 +2,19 @@
|
|||
"batterySettings": {
|
||||
"batteryLevel": "Nivell de bateria",
|
||||
"bufferStart": {
|
||||
"full": "quan gairebé ple",
|
||||
"full": "quan gairebé ple {soc}.",
|
||||
"never": "només amb prous excedents"
|
||||
},
|
||||
"capacity": "{energy} de {total}",
|
||||
"disclaimerHint": "Nota:",
|
||||
"disclaimerText": "aquests paràmetres només afecten el mode solar. El comportament de càrrega s'ajusta en conseqüència.",
|
||||
"legendBottomName": "prioritat de la casa",
|
||||
"legendBottomSubline": "no utilitzat per carregar",
|
||||
"legendBottomSubline": "fins que arriba a {soc}.",
|
||||
"legendMiddleName": "primer, el vehicle",
|
||||
"legendMiddleSubline": "segón, la casa",
|
||||
"legendMiddleSubline": "quan la bateria de casa està per sobre {soc}.",
|
||||
"legendTopAutostart": "s'inicia automaticament",
|
||||
"legendTopName": "càrrega soportada per bateria",
|
||||
"legendTopSubline": "sensa interrupcions",
|
||||
"legendTopSubline": "quan la bateria de casa està per sobre {soc}.",
|
||||
"modalTitle": "Configuració bateria"
|
||||
},
|
||||
"footer": {
|
||||
|
|
@ -217,8 +217,8 @@
|
|||
"pvEnable": "Excedent disponible. Començar en {remaining}...",
|
||||
"scale1p": "Reduint a càrrega monofàsica a {remaining}...",
|
||||
"scale3p": "Augmentant la càrrega trifàsica en {remaining}...",
|
||||
"targetChargeActive": "Càrrega objectiu activa...",
|
||||
"targetChargePlanned": "La càrrega objectiu comença a {time}.",
|
||||
"targetChargeActive": "Càrrega objectiu activa. Finalització estimada en {duration}.",
|
||||
"targetChargePlanned": "La càrrega objectiu comença en {duration}.",
|
||||
"targetChargeWaitForVehicle": "Càrrega objectiu llesta. Esperar vehicle.",
|
||||
"vehicleLimitReached": "Límit de vehicle {soc} assolit.",
|
||||
"waitForVehicle": "A punt per carregar. Esperant vehicle."
|
||||
|
|
|
|||
157
i18n/check.ts
Normal file
157
i18n/check.ts
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
#!/usr/bin/env tsx
|
||||
|
||||
import { readFileSync, readdirSync, existsSync } from "fs";
|
||||
import { join, dirname, basename, extname, resolve } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Security: Validate that file path is within the i18n directory
|
||||
function validateFilePath(filePath: string): string {
|
||||
const resolvedPath = resolve(filePath);
|
||||
const resolvedBaseDir = resolve(__dirname);
|
||||
|
||||
if (!resolvedPath.startsWith(resolvedBaseDir)) {
|
||||
throw new Error(`Path traversal attempt detected: ${filePath}`);
|
||||
}
|
||||
|
||||
// Ensure it's a JSON file
|
||||
if (extname(resolvedPath) !== ".json") {
|
||||
throw new Error(`Invalid file type: ${filePath}`);
|
||||
}
|
||||
|
||||
return resolvedPath;
|
||||
}
|
||||
|
||||
// Extract placeholders from a translation string (e.g., "{title}", "{soc}", etc.)
|
||||
function extractPlaceholders(text: string): string[] {
|
||||
const matches = text.match(/\{[^}]+\}/g);
|
||||
if (!matches) return [];
|
||||
|
||||
// Remove duplicates and sort
|
||||
const unique = [...new Set(matches)];
|
||||
return unique.sort();
|
||||
}
|
||||
|
||||
// Flatten nested translation object into dot-notation keys
|
||||
function flattenObject(obj: any, prefix = ""): Record<string, string> {
|
||||
return Object.entries(obj).reduce((acc, [key, val]) => {
|
||||
const path = prefix ? `${prefix}.${key}` : key;
|
||||
if (val && typeof val === "object") {
|
||||
Object.assign(acc, flattenObject(val, path));
|
||||
} else if (typeof val === "string") {
|
||||
acc[path] = val;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
}
|
||||
|
||||
// Compare placeholder arrays using Set equality for better performance
|
||||
function placeholdersMatch(a: string[], b: string[]): boolean {
|
||||
const setA = new Set(a);
|
||||
const setB = new Set(b);
|
||||
if (setA.size !== setB.size) return false;
|
||||
for (const placeholder of setA) {
|
||||
if (!setB.has(placeholder)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Load and parse JSON translation file safely
|
||||
function loadTranslationFile(filePath: string): Record<string, string> {
|
||||
const validatedPath = validateFilePath(filePath);
|
||||
const content = readFileSync(validatedPath, "utf-8");
|
||||
const parsed = JSON.parse(content);
|
||||
return flattenObject(parsed);
|
||||
}
|
||||
|
||||
// Get all translation files in the directory safely
|
||||
function getTranslationFiles(): string[] {
|
||||
const files = readdirSync(__dirname)
|
||||
.filter(file => {
|
||||
// Only allow valid filename characters and JSON extension
|
||||
return /^[a-zA-Z0-9_-]+\.json$/.test(file);
|
||||
})
|
||||
.map(file => validateFilePath(join(__dirname, file)));
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
// Main validation function
|
||||
function validateTranslations(): void {
|
||||
console.log("🔍 Checking translation placeholder consistency...");
|
||||
|
||||
const sourceFile = validateFilePath(join(__dirname, "en.json"));
|
||||
|
||||
if (!existsSync(sourceFile)) {
|
||||
console.error(`❌ Source file not found: ${sourceFile}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const sourceFlat = loadTranslationFile(sourceFile);
|
||||
const translationFiles = getTranslationFiles()
|
||||
.filter(file => basename(file) !== "en.json");
|
||||
|
||||
if (translationFiles.length === 0) {
|
||||
console.log("ℹ️ No translation files to check.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const allErrors: Array<{file: string; key: string; expected: string[]; found: string[]}> = [];
|
||||
|
||||
for (const file of translationFiles) {
|
||||
const lang = basename(file, ".json");
|
||||
const targetFlat = loadTranslationFile(file);
|
||||
|
||||
for (const [key, sourceStr] of Object.entries(sourceFlat)) {
|
||||
const sourcePlaceholders = extractPlaceholders(sourceStr);
|
||||
|
||||
// Only check if source has placeholders
|
||||
if (sourcePlaceholders.length === 0) continue;
|
||||
|
||||
// Skip if translation doesn't exist at all (fallback to English is fine)
|
||||
if (!(key in targetFlat)) continue;
|
||||
|
||||
const targetPlaceholders = extractPlaceholders(targetFlat[key]);
|
||||
|
||||
if (!placeholdersMatch(sourcePlaceholders, targetPlaceholders)) {
|
||||
allErrors.push({
|
||||
file: lang,
|
||||
key,
|
||||
expected: sourcePlaceholders,
|
||||
found: targetPlaceholders
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Group errors by file and output
|
||||
const errorsByFile = new Map<string, typeof allErrors>();
|
||||
for (const error of allErrors) {
|
||||
if (!errorsByFile.has(error.file)) {
|
||||
errorsByFile.set(error.file, []);
|
||||
}
|
||||
errorsByFile.get(error.file)!.push(error);
|
||||
}
|
||||
|
||||
for (const [lang, errors] of errorsByFile) {
|
||||
console.log(`\n📄 i18n/${lang}.json (${errors.length} errors)`);
|
||||
for (const error of errors) {
|
||||
console.log(` ${error.key}`);
|
||||
console.log(` expected: ${error.expected.join(", ")}`);
|
||||
console.log(` found: ${error.found.join(", ") || "(none)"}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (allErrors.length > 0) {
|
||||
console.log(`\n💡 ${allErrors.length} placeholder errors found.`);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log("✅ All translations have correct placeholders!");
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the validation
|
||||
validateTranslations();
|
||||
|
|
@ -429,7 +429,7 @@
|
|||
"addToken": "Token beírása",
|
||||
"changeToken": "Token megváltoztatása",
|
||||
"description": "A szponzorációs modell segít a projekt fenntartásában és az új izgalmas funkciók bevezetésében. Szponzorként teljes hozzáférést kapsz az összes töltőberendezés implementációjához.",
|
||||
"descriptionToken": "A tokent innen kapja: {url}. A teszteléshez próba tokent is kínálunk. ",
|
||||
"descriptionToken": "A tokent innen kapja: {url}. A teszteléshez {trialToken} tokent is kínálunk.",
|
||||
"enterYourToken": "Add meg a tokenedet",
|
||||
"error": "A szponzor token nem érvényes.",
|
||||
"invalid": "érvénytelen",
|
||||
|
|
|
|||
|
|
@ -825,7 +825,7 @@
|
|||
"onlyInPvMode": "Luedplanifikatioun ass nëmmen am PV-Modus aktiv.",
|
||||
"planDuration": "Dauer vum Oplueden",
|
||||
"planPeriodLabel": "Zäitraum",
|
||||
"planPeriodValue": "{Start} bis {end}",
|
||||
"planPeriodValue": "{start} bis {end}",
|
||||
"planUnknown": "nach net bekannt",
|
||||
"preview": "Virschau vun der Planifikatioun",
|
||||
"priceLimit": "Präisgrenz vu {price}",
|
||||
|
|
@ -890,7 +890,7 @@
|
|||
"scale1p": "Reduktioun op eng eenzeg Phase.",
|
||||
"scale3p": "Erhéije geschwënn op dräi Phasen.",
|
||||
"targetChargeActive": "Luedplanung aktiv. Ageschate Schluss an {duration}.",
|
||||
"targetChargePlanned": "Luedpladung fänkt un an {time} un.",
|
||||
"targetChargePlanned": "Luedpladung fänkt un an {duration} un.",
|
||||
"targetChargeWaitForVehicle": "Luedplang ass prett. Waarden op d'Gefier…",
|
||||
"vehicleLimit": "Limitt vum Gefier",
|
||||
"vehicleLimitReached": "Limitt vum Gefier erreecht.",
|
||||
|
|
|
|||
|
|
@ -429,7 +429,7 @@
|
|||
"addToken": "Token invoeren",
|
||||
"changeToken": "Token bewerken",
|
||||
"description": "Het sponsormodel helpt ons het project te onderhouden en nieuwe toffe functies te ontwikkelen. Als sponsor krijg je toegang tot de implementaties voor alle laadpunten.",
|
||||
"descriptionToken": "Sponsors vinden hun token op {url}. Om te starten, bieden we een ",
|
||||
"descriptionToken": "Sponsors vinden hun token op {url}. Om te starten, bieden we een {trialToken}.",
|
||||
"enterYourToken": "Typ hier je token",
|
||||
"error": "De sponsortoken is ongeldig.",
|
||||
"invalid": "ongeldig",
|
||||
|
|
@ -693,7 +693,7 @@
|
|||
"modalTitle": "Laad Plan",
|
||||
"none": "geen",
|
||||
"planNumber": "Schema {number}",
|
||||
"preconditionDescription": "Laad(duur) voor vertrek voor accu voorbereiding.",
|
||||
"preconditionDescription": "Laad {duration} voor vertrek voor accu voorbereiding.",
|
||||
"preconditionLong": "Laat Opladen",
|
||||
"preconditionOptionAll": "alles",
|
||||
"preconditionOptionNo": "nee",
|
||||
|
|
|
|||
16
i18n/no.json
16
i18n/no.json
|
|
@ -2,19 +2,19 @@
|
|||
"batterySettings": {
|
||||
"batteryLevel": "Ladenivå",
|
||||
"bufferStart": {
|
||||
"full": "når det er nesten fullt",
|
||||
"full": "når det er på {soc}.",
|
||||
"never": "bare med nok PV-overskudd"
|
||||
},
|
||||
"capacity": "{energy} av {total}",
|
||||
"disclaimerHint": "Merk:",
|
||||
"disclaimerText": "Kun relevant i PV-modus. Ladeadferden justeres deretter.",
|
||||
"legendBottomName": "Hjemmelading har prioritet",
|
||||
"legendBottomSubline": "ikke bruk til lading",
|
||||
"legendBottomSubline": "til det når {soc}.",
|
||||
"legendMiddleName": "kjøretøy først",
|
||||
"legendMiddleSubline": "hjemme etterpå",
|
||||
"legendMiddleSubline": "når hjemmebatteriet er over {soc}.",
|
||||
"legendTopAutostart": "starter automatisk",
|
||||
"legendTopName": "batteriassistert lading",
|
||||
"legendTopSubline": "uten avbrudd",
|
||||
"legendTopSubline": "når hjemmebatteriet er over {soc}.",
|
||||
"modalTitle": "Batteriinnstillinger"
|
||||
},
|
||||
"config": {
|
||||
|
|
@ -66,7 +66,7 @@
|
|||
"footerLong": "{percent} solcelleenergi",
|
||||
"footerShort": "{percent} solenergi",
|
||||
"modalTitle": "Oversikt over ladeenergi",
|
||||
"percentGrid": "{self} kWh lysnett",
|
||||
"percentGrid": "{grid} kWh lysnett",
|
||||
"percentSelf": "{self} kWt sol",
|
||||
"percentTitle": "Solenergi",
|
||||
"priceTitle": "Kraftpris",
|
||||
|
|
@ -253,8 +253,8 @@
|
|||
"pvEnable": "Overskudd tilgjengelig. Starter om {remaining}...",
|
||||
"scale1p": "Reduserer til 1-fase strøm i {remaining}...",
|
||||
"scale3p": "Øker til 3-fase strøm i {remaining}...",
|
||||
"targetChargeActive": "Målladning aktiv ...",
|
||||
"targetChargePlanned": "Mållading starter på {time}.",
|
||||
"targetChargeActive": "Målladning aktiv. Estimert ferdig om {duration}.",
|
||||
"targetChargePlanned": "Mållading starter på {duration}.",
|
||||
"targetChargeWaitForVehicle": "Mållading klar. Venter på kjøretøy...",
|
||||
"vehicleLimitReached": "Kjøretøygrense på {soc} nådd.",
|
||||
"waitForVehicle": "Klar. Venter på kjøretøy..."
|
||||
|
|
@ -298,7 +298,7 @@
|
|||
"odometer": "Kilometerstand",
|
||||
"vehicle": "Kjøretøy"
|
||||
},
|
||||
"csvPeriod": "Last ned (måned) CSV",
|
||||
"csvPeriod": "Last ned {period} CSV",
|
||||
"csvTotal": "Last ned total CSV",
|
||||
"date": "Start",
|
||||
"energy": "Oppladet",
|
||||
|
|
|
|||
14
i18n/ro.json
14
i18n/ro.json
|
|
@ -3,7 +3,7 @@
|
|||
"batteryLevel": "Nivelul bateriei",
|
||||
"bufferStart": {
|
||||
"above": "cand e peste {soc}",
|
||||
"full": "cand atinge starea de incarcare",
|
||||
"full": "când atinge {soc}.",
|
||||
"never": "numai cand exista surplus suficient"
|
||||
},
|
||||
"capacity": "{energy} din {total}",
|
||||
|
|
@ -12,12 +12,12 @@
|
|||
"disclaimerHint": "Nota:",
|
||||
"disclaimerText": "Aceste setari afecteaza doar modul solar. Procesul de incarcare este ajustat corespunzator",
|
||||
"legendBottomName": "prioritate casa",
|
||||
"legendBottomSubline": "neutilizat pentru incarcare",
|
||||
"legendBottomSubline": "până când atinge {soc}.",
|
||||
"legendMiddleName": "in primul rand vehiculul",
|
||||
"legendMiddleSubline": "in al doilea rand casa",
|
||||
"legendMiddleSubline": "când bateria casei e peste {soc}.",
|
||||
"legendTopAutostart": "porneste automat",
|
||||
"legendTopName": "Incarcare ajutata de baterie",
|
||||
"legendTopSubline": "fara intreruperi",
|
||||
"legendTopSubline": "când bateria casei e peste {soc}.",
|
||||
"modalTitle": "Setari bateriei"
|
||||
},
|
||||
"config": {
|
||||
|
|
@ -278,7 +278,7 @@
|
|||
"descriptionEnergy": "Pâna când ar trebui ca {targetEnergy} să fie încărcată in vehicul?",
|
||||
"descriptionSoc": "Când ar trebui vehiculul să fie încărcat la {targetSoc}?",
|
||||
"inactiveLabel": "Timp dorit",
|
||||
"notReachableInTime": "Telul nu a fost atins la timp. Estimare: {endTime}.",
|
||||
"notReachableInTime": "Telul nu a fost atins la timp. Estimare: {overrun}.",
|
||||
"onlyInPvMode": "Planul de incarcare merge doar in mod Solar.",
|
||||
"planDuration": "Timp de încărcare",
|
||||
"planPeriodLabel": "Perioada",
|
||||
|
|
@ -335,8 +335,8 @@
|
|||
"pvEnable": "Surplus FV disponibil. Pornesc în {remaining}…",
|
||||
"scale1p": "Reduc încărcarea la faza 1 în {remaining}…",
|
||||
"scale3p": "Schimbam încărcarea la 3 faze în {remaining}…",
|
||||
"targetChargeActive": "Încarcare țintă activă…",
|
||||
"targetChargePlanned": "Încărcarea pornește la {time}.",
|
||||
"targetChargeActive": "Încărcare țintă activă. Terminare estimată în {duration}.",
|
||||
"targetChargePlanned": "Încărcarea pornește la {duration}.",
|
||||
"targetChargeWaitForVehicle": "Încărcare terminată. Aștept după vehicul…",
|
||||
"vehicleLimitReached": "Limita {soc} a fost atinsă.",
|
||||
"waitForVehicle": "Pregătit. Aștept după vehicul…"
|
||||
|
|
|
|||
14
i18n/ru.json
14
i18n/ru.json
|
|
@ -2,7 +2,7 @@
|
|||
"batterySettings": {
|
||||
"batteryLevel": "Уровень заряда батареи",
|
||||
"bufferStart": {
|
||||
"full": "когда почти полная",
|
||||
"full": "когда на {soc}.",
|
||||
"never": "только при достаточном избытке"
|
||||
},
|
||||
"capacity": "{energy} из {total}",
|
||||
|
|
@ -11,12 +11,12 @@
|
|||
"disclaimerHint": "Примечание:",
|
||||
"disclaimerText": "Эти настройки влияют только на режим солнечной батареи. Поведение зарядки изменяется соответствующе.",
|
||||
"legendBottomName": "домашний приоритет",
|
||||
"legendBottomSubline": "не используется для зарядки",
|
||||
"legendBottomSubline": "пока не достигнет {soc}.",
|
||||
"legendMiddleName": "сначала автомобиль",
|
||||
"legendMiddleSubline": "дом вторым",
|
||||
"legendMiddleSubline": "когда домашняя батарея выше {soc}.",
|
||||
"legendTopAutostart": "запускается автоматически",
|
||||
"legendTopName": "зарядка с поддержкой от аккумулятора",
|
||||
"legendTopSubline": "без перебоев",
|
||||
"legendTopSubline": "когда домашняя батарея выше {soc}.",
|
||||
"modalTitle": "Настройки батареи"
|
||||
},
|
||||
"config": {
|
||||
|
|
@ -48,7 +48,7 @@
|
|||
"footerLong": "{percent} солнечной энергии",
|
||||
"footerShort": "{percent} солнечной",
|
||||
"modalTitle": "Обзор потребленной электроэнергии",
|
||||
"percentGrid": "{self} кВт/ч из сети",
|
||||
"percentGrid": "{grid} кВт/ч из сети",
|
||||
"percentSelf": "{self} кВт/ч солнечной",
|
||||
"percentTitle": "Солнечная энергия",
|
||||
"priceTitle": "Цена электроэнергии",
|
||||
|
|
@ -175,8 +175,8 @@
|
|||
"pvEnable": "Доступен избыток. Начинаю через {remaining}...",
|
||||
"scale1p": "Снижаю до одной фазы через {remaining}...",
|
||||
"scale3p": "Увеличиваю до трех фаз через {remaining}...",
|
||||
"targetChargeActive": "Плановая зарядка активна...",
|
||||
"targetChargePlanned": "Плановая зарядка начнется в {time}.",
|
||||
"targetChargeActive": "Плановая зарядка активна. Ориентировочное завершение через {duration}.",
|
||||
"targetChargePlanned": "Плановая зарядка начнется в {duration}.",
|
||||
"targetChargeWaitForVehicle": "Плановая зарядка готова. Ожидаю транспортное средство...",
|
||||
"waitForVehicle": "Готов. Ожидание транспортного средства..."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -481,7 +481,7 @@
|
|||
"today": "Danes",
|
||||
"tomorrow": "Jutri"
|
||||
},
|
||||
"solarAdjust": "Prilagodite sončno napoved na podlagi dejanskih podatkov o proizvodnji.",
|
||||
"solarAdjust": "Prilagodite sončno napoved na podlagi dejanskih podatkov o proizvodnji {percent}.",
|
||||
"type": {
|
||||
"co2": "CO₂",
|
||||
"price": "Cena",
|
||||
|
|
@ -729,7 +729,7 @@
|
|||
"scale1p": "Zmanjšanje na 1-fazno napajanje kmalu...",
|
||||
"scale3p": "Povečanje na 3-fazno napajanje kmalu...",
|
||||
"targetChargeActive": "Ciljno polnjenje je aktivno, predviden konec {duration}.",
|
||||
"targetChargePlanned": "Ciljno polnjenje se začne ob {time}.",
|
||||
"targetChargePlanned": "Ciljno polnjenje se začne čez {duration}.",
|
||||
"targetChargeWaitForVehicle": "Ciljno polnjenje je pripravljeno. Čakam na vozilo ...",
|
||||
"vehicleLimit": "Omejitev vozila",
|
||||
"vehicleLimitReached": "Omejitev vozila dosežena.",
|
||||
|
|
|
|||
|
|
@ -822,7 +822,7 @@
|
|||
"goalReached": "Målet är uppnått",
|
||||
"inactiveLabel": "Mål-tid",
|
||||
"nextPlan": "Nästa plan",
|
||||
"notReachableInTime": "Laddmål kommer att nås {endTime} senare.",
|
||||
"notReachableInTime": "Laddmål kommer att nås {overrun} senare.",
|
||||
"onlyInPvMode": "Laddplan fungerar enbart i sol-läge.",
|
||||
"planDuration": "Laddtid",
|
||||
"planPeriodLabel": "Period",
|
||||
|
|
|
|||
16
i18n/ta.json
16
i18n/ta.json
|
|
@ -3,7 +3,7 @@
|
|||
"batteryLevel": "பேட்டரி நிலை",
|
||||
"bufferStart": {
|
||||
"above": "மேலே {soc}.",
|
||||
"full": "போது {soc at இல் இருக்கும்போது.",
|
||||
"full": "{soc} இல் இருக்கும்போது.",
|
||||
"never": "போதுமான உபரி மட்டுமே."
|
||||
},
|
||||
"capacity": "{energy} இன் {total} \"",
|
||||
|
|
@ -277,7 +277,7 @@
|
|||
"thresholdDisableLabel": "கட்டம் சக்தியை முடக்கு",
|
||||
"thresholdEnableDelayLabel": "தாமதத்தை இயக்கவும்",
|
||||
"thresholdEnableHelpInvalid": "எதிர்மறை மதிப்பைப் பயன்படுத்தவும்.",
|
||||
"thresholdEnableHelpNegative": "{surplus} உபரி {தாமதத்திற்கு கிடைக்கும்போது தொடங்குங்கள்.",
|
||||
"thresholdEnableHelpNegative": "{surplus} உபரி {delay} தாமதத்திற்கு கிடைக்கும்போது தொடங்குங்கள்.",
|
||||
"thresholdEnableHelpZero": "{delay} க்கு குறைந்தபட்ச தேவையான சக்தியை நிறைவு செய்யும்போது தொடங்கவும்.",
|
||||
"thresholdEnableLabel": "கட்டம் சக்தியை இயக்கவும்",
|
||||
"titleAdd": {
|
||||
|
|
@ -429,7 +429,7 @@
|
|||
"addToken": "ஒப்புரவாளர் கிள்ளாக்கை உள்ளிடவும்",
|
||||
"changeToken": "ஒப்புரவாளர் கிள்ளாக்கை மாற்றவும்",
|
||||
"description": "ஒப்புரவாளர் மாதிரி திட்டத்தை பராமரிக்கவும் புதிய மற்றும் அற்புதமான அம்சங்களை நிலையானதாக உருவாக்கவும் உதவுகிறது. ஒரு ச்பான்சராக நீங்கள் அனைத்து சார்சர் செயலாக்கங்களுக்கும் அணுகலைப் பெறுவீர்கள்.",
|
||||
"descriptionToken": "நீங்கள் {url இருந்து இலிருந்து கிள்ளாக்கைப் பெறுவீர்கள். சோதனைக்கு ஒரு சோதனை டோக்கனையும் நாங்கள் வழங்குகிறோம்.",
|
||||
"descriptionToken": "ஸ்பான்சர்கள் {url} இலிருந்து டோக்கனைப் பெறுவீர்கள். தொடங்குவதற்கு {trialToken} வழங்குகிறோம்.",
|
||||
"error": "ஒப்புரவாளர் கிள்ளாக்கு செல்லுபடியாகாது.",
|
||||
"labelToken": "ஒப்புரவாளர் கிள்ளாக்கு",
|
||||
"title": "ச்பான்சர்சிப்",
|
||||
|
|
@ -530,7 +530,7 @@
|
|||
"greenShareSub1": "வழங்கிய ஆற்றல்",
|
||||
"greenShareSub2": "சூரிய மற்றும் பேட்டரி சேமிப்பு",
|
||||
"power": "கட்டணம் வசூலிக்கும் ஆற்றல்",
|
||||
"powerSub1": "{செயலில் உள்ள வாடிக்கையாளர்கள் {activeClients} பங்கேற்பாளர்கள்",
|
||||
"powerSub1": "{activeClients} of {totalClients} பங்கேற்பாளர்கள்",
|
||||
"powerSub2": "சார்சிங்…",
|
||||
"tabTitle": "நேரடி சமூகம்"
|
||||
},
|
||||
|
|
@ -684,7 +684,7 @@
|
|||
"modalTitle": "மின்சேர்வி திட்டம்",
|
||||
"none": "எதுவுமில்லை",
|
||||
"planNumber": "திட்டம் {number}",
|
||||
"preconditionDescription": "பேட்டரி முன்நிபந்தனைக்கு புறப்படுவதற்கு முன் {காலம் {காலம்.",
|
||||
"preconditionDescription": "பேட்டரி முன்நிபந்தனைக்கு புறப்படுவதற்கு முன் {duration} சார்ஜ் செய்யவும்.",
|
||||
"preconditionLong": "தாமதமாக சார்சிங்",
|
||||
"preconditionOptionAll": "எல்லாம்",
|
||||
"preconditionOptionNo": "இல்லை",
|
||||
|
|
@ -864,7 +864,7 @@
|
|||
"feedinPriorityNextStart": "அதிக தீவன விகிதங்கள் {duration} இல் தொடங்குகின்றன.",
|
||||
"feedinPriorityPausing": "சூரிய சார்ச் ஊட்டத்தை அதிகரிக்க இடைநிறுத்தப்பட்டது.",
|
||||
"finished": "முடிந்தது.",
|
||||
"minCharge": "{Soc} க்கு குறைந்தஅளவு கட்டணம்.",
|
||||
"minCharge": "{soc} க்கு குறைந்தஅளவு கட்டணம்.",
|
||||
"pvDisable": "போதுமான உபரி இல்லை. விரைவில் இடைநிறுத்துதல்.",
|
||||
"pvEnable": "உபரி கிடைக்கிறது. விரைவில் தொடங்குகிறது.",
|
||||
"scale1p": "விரைவில் 1-கட்ட கட்டணம் வசூலிப்பதைக் குறைத்தல்.",
|
||||
|
|
@ -1055,7 +1055,7 @@
|
|||
"priceLabel": "ஆற்றல் விலை",
|
||||
"priceLimit": "விலை வரம்பு",
|
||||
"resetAction": "வரம்பை அகற்று",
|
||||
"resetWarning": "மாறும் கட்டம் விலை அல்லது கோ-மூல கட்டமைக்கப்படவில்லை. இருப்பினும், {வரம்பு of இன் வரம்பு இன்னும் உள்ளது. உங்கள் உள்ளமைவை தூய்மை செய்யவா?",
|
||||
"resetWarning": "மாறும் கிரிட் விலை அல்லது CO₂ மூலம் கட்டமைக்கப்படவில்லை. ஆனால், {limit} வரம்பு இன்னும் உள்ளது. உங்கள் கட்டமைவை சுத்தம் செய்யவா?",
|
||||
"saved": "சேமிக்கப்பட்டது."
|
||||
},
|
||||
"smartFeedInPriority": {
|
||||
|
|
@ -1063,7 +1063,7 @@
|
|||
"description": "இலாபகரமான கட்டம் ஊட்டத்திற்கு முன்னுரிமை அளிக்க அதிக விலையில் கட்டணம் வசூலிப்பதை இடைநிறுத்துகிறது.",
|
||||
"priceLabel": "தீவன வீதம்",
|
||||
"priceLimit": "தீவன வரம்பு",
|
||||
"resetWarning": "மாறும் ஃபீட்-இன் கட்டணம் எதுவும் கட்டப்படவில்லை. இருப்பினும், {வரம்பு of இன் வரம்பு இன்னும் உள்ளது. உங்கள் உள்ளமைவை தூய்மை செய்யவா?",
|
||||
"resetWarning": "மாறும் ஃபீட்-இன் டாரிஃப் கட்டமைக்கப்படவில்லை. ஆனால், {limit} வரம்பு இன்னும் உள்ளது. உங்கள் கட்டமைவை சுத்தம் செய்யவா?",
|
||||
"title": "தீவன முன்னுரிமை"
|
||||
},
|
||||
"startupError": {
|
||||
|
|
|
|||
|
|
@ -794,7 +794,7 @@
|
|||
"scale1p": "准备切换为单相充电。",
|
||||
"scale3p": "准备切换为三相充电。",
|
||||
"targetChargeActive": "充电计划已激活。预计在 {duration} 内完成。",
|
||||
"targetChargePlanned": "充电计划将于 {duration} 后开始({time})。",
|
||||
"targetChargePlanned": "充电计划将于 {duration} 后开始。",
|
||||
"targetChargeWaitForVehicle": "充电计划准备就绪。等待车辆连接…",
|
||||
"vehicleLimit": "车辆充电上限",
|
||||
"vehicleLimitReached": "已达到车辆充电上限 {soc}。",
|
||||
|
|
|
|||
59
package-lock.json
generated
59
package-lock.json
generated
|
|
@ -67,6 +67,7 @@
|
|||
"rollup-plugin-visualizer": "^6.0.3",
|
||||
"storybook": "^10.0.0-beta.2",
|
||||
"terser": "^5.44.0",
|
||||
"tsx": "^4.20.5",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript-eslint-language-service": "^5.0.5",
|
||||
"vite": "^7.1.5",
|
||||
|
|
@ -6579,6 +6580,19 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/get-tsconfig": {
|
||||
"version": "4.10.1",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz",
|
||||
"integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"resolve-pkg-maps": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||
|
|
@ -9164,6 +9178,16 @@
|
|||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-pkg-maps": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/reusify": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
||||
|
|
@ -10341,6 +10365,41 @@
|
|||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.20.5",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.5.tgz",
|
||||
"integrity": "sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "~0.25.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
},
|
||||
"bin": {
|
||||
"tsx": "dist/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
|
|
|
|||
|
|
@ -5,10 +5,11 @@
|
|||
"scripts": {
|
||||
"build": "vite build",
|
||||
"test": "cross-env TZ=Europe/Berlin vitest",
|
||||
"lint": "npm run lint:prettier && npm run lint:eslint && npm run lint:tsc",
|
||||
"lint": "npm run lint:prettier && npm run lint:eslint && npm run lint:tsc && npm run lint:i18n",
|
||||
"lint:prettier": "prettier assets tests **/*.{yaml,sh} i18n/*.json --write",
|
||||
"lint:eslint": "eslint --fix --max-warnings=0",
|
||||
"lint:tsc": "vue-tsc --noEmit",
|
||||
"lint:i18n": "tsx i18n/check.ts",
|
||||
"dev": "vite",
|
||||
"playwright": "playwright test --ui",
|
||||
"simulator": "vite tests/simulator",
|
||||
|
|
@ -66,7 +67,6 @@
|
|||
"@vue/tsconfig": "^0.8.1",
|
||||
"body-parser": "^2.2.0",
|
||||
"cross-env": "^10.0.0",
|
||||
"license-compliance": "^3.0.1",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
|
|
@ -77,6 +77,7 @@
|
|||
"globals": "^16.3.0",
|
||||
"happy-dom": "^18.0.1",
|
||||
"kill-port": "^2.0.1",
|
||||
"license-compliance": "^3.0.1",
|
||||
"mqtt": "^5.14.1",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-sh": "^0.18.0",
|
||||
|
|
@ -84,6 +85,7 @@
|
|||
"rollup-plugin-visualizer": "^6.0.3",
|
||||
"storybook": "^10.0.0-beta.2",
|
||||
"terser": "^5.44.0",
|
||||
"tsx": "^4.20.5",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript-eslint-language-service": "^5.0.5",
|
||||
"vite": "^7.1.5",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue