-
+
+
+
@@ -584,33 +607,32 @@
Home
-
-
-
-
-
-
+
+
+
+
-
+
+
-
+
-
+
-
-
+
+
-
+
-
+
diff --git a/selfdrive/carrot/web/js/hud_card.js b/selfdrive/carrot/web/js/hud_card.js
index b6e0dd3232..c56188e430 100644
--- a/selfdrive/carrot/web/js/hud_card.js
+++ b/selfdrive/carrot/web/js/hud_card.js
@@ -38,27 +38,11 @@
sport: "高速",
fast: "高速",
},
- ja: {
- normal: "通常",
- eco: "エコ",
- safe: "安全",
- sport: "高速",
- fast: "高速",
- },
- fr: {
- normal: "Normal",
- eco: "Eco",
- safe: "Securite",
- sport: "Rapide",
- fast: "Rapide",
- },
};
const HUD_LABELS = {
en: { speed: "Speed", setSpeed: "Set Speed", temp: "TEMP", gear: "GEAR", limit: "LIMIT" },
ko: { speed: "현재속도", setSpeed: "설정속도", temp: "TEMP", gear: "GEAR", limit: "LIMIT" },
zh: { speed: "当前速度", setSpeed: "设定速度", temp: "TEMP", gear: "GEAR", limit: "LIMIT" },
- ja: { speed: "速度", setSpeed: "設定速度", temp: "TEMP", gear: "GEAR", limit: "LIMIT" },
- fr: { speed: "Vitesse", setSpeed: "Vitesse reglee", temp: "TEMP", gear: "GEAR", limit: "LIMIT" },
};
const HUD_AUX_ROTATE_MS = 1600;
const HUD_AUX_ICON_PATHS = {
diff --git a/selfdrive/carrot/web/js/pages/setting.js b/selfdrive/carrot/web/js/pages/setting.js
index 0677ae65a0..cbde3d3611 100644
--- a/selfdrive/carrot/web/js/pages/setting.js
+++ b/selfdrive/carrot/web/js/pages/setting.js
@@ -16,6 +16,8 @@ let settingSubnavProgrammaticScroll = false;
let settingSubnavFocusTimer = null;
const SETTING_FAVORITES_GROUP = "__setting_favorites__";
+const SETTING_PROFILES_DIVIDER = "__setting_profiles_divider__";
+const SETTING_PROFILE_GROUP_PREFIX = "__setting_profile__:";
const SETTING_FAVORITES_LONG_PRESS_MS = 620;
const SETTING_FAVORITES_MOVE_TOLERANCE = 10;
const settingFavoritesState = {
@@ -23,11 +25,42 @@ const settingFavoritesState = {
loaded: false,
loadPromise: null,
};
+const settingProfilesState = {
+ profiles: [],
+ loaded: false,
+ loadPromise: null,
+};
+const settingProfileSectionExpandedState = new Map();
function isSettingFavoritesGroup(group) {
return group === SETTING_FAVORITES_GROUP;
}
+function isSettingProfilesDivider(entry) {
+ return entry?.group === SETTING_PROFILES_DIVIDER || entry === SETTING_PROFILES_DIVIDER;
+}
+
+function settingProfileGroup(profileId) {
+ return SETTING_PROFILE_GROUP_PREFIX + String(profileId || "");
+}
+
+function isSettingProfileGroup(group) {
+ return String(group || "").startsWith(SETTING_PROFILE_GROUP_PREFIX);
+}
+
+function getSettingProfileIdFromGroup(group) {
+ return isSettingProfileGroup(group) ? String(group).slice(SETTING_PROFILE_GROUP_PREFIX.length) : "";
+}
+
+function getSettingProfileById(profileId) {
+ const id = String(profileId || "");
+ return settingProfilesState.profiles.find((profile) => profile?.id === id) || null;
+}
+
+function getSettingProfileByGroup(group) {
+ return getSettingProfileById(getSettingProfileIdFromGroup(group));
+}
+
function normalizeSettingFavoriteNames(names) {
const out = [];
const seen = new Set();
@@ -57,6 +90,32 @@ function getFavoriteSettingEntries() {
.filter(Boolean);
}
+function getSettingGroupOrderIndex(group) {
+ const groups = SETTINGS?.groups || [];
+ const index = groups.findIndex((entry) => entry?.group === group);
+ return index >= 0 ? index : 9999;
+}
+
+function getSettingItemOrderIndex(group, name) {
+ const list = SETTINGS?.items_by_group?.[group] || [];
+ const index = list.findIndex((entry) => entry?.name === name);
+ return index >= 0 ? index : 9999;
+}
+
+function getProfileSettingEntries(profile) {
+ const values = profile?.values || {};
+ return Object.keys(values)
+ .map((name) => findSettingItemByName(name))
+ .filter(Boolean)
+ .sort((a, b) => {
+ const groupDelta = getSettingGroupOrderIndex(a.group) - getSettingGroupOrderIndex(b.group);
+ if (groupDelta) return groupDelta;
+ const itemDelta = getSettingItemOrderIndex(a.group, a.item.name) - getSettingItemOrderIndex(b.group, b.item.name);
+ if (itemDelta) return itemDelta;
+ return String(a.item.name).localeCompare(String(b.item.name));
+ });
+}
+
function getValidSettingFavoriteNames() {
return getFavoriteSettingEntries().map((entry) => entry.item.name).filter(Boolean);
}
@@ -69,9 +128,13 @@ function getSettingFavoritesLabel() {
return getUIText("setting_favorites", "Favorites");
}
+function getSettingProfilesLabel() {
+ return getUIText("setting_profiles", "Profiles");
+}
+
function getSettingGroupsForDisplay() {
const groups = SETTINGS?.groups || [];
- return [
+ const out = [
{
group: SETTING_FAVORITES_GROUP,
count: getFavoriteSettingEntries().length,
@@ -79,10 +142,31 @@ function getSettingGroupsForDisplay() {
},
...groups,
];
+ const profiles = settingProfilesState.profiles || [];
+ if (profiles.length) {
+ out.push({
+ group: SETTING_PROFILES_DIVIDER,
+ label: getSettingProfilesLabel(),
+ divider: true,
+ virtual: true,
+ });
+ profiles.forEach((profile) => {
+ out.push({
+ group: settingProfileGroup(profile.id),
+ count: getProfileSettingEntries(profile).length,
+ label: profile.name,
+ profile,
+ virtual: true,
+ });
+ });
+ }
+ return out;
}
function getSettingItemEntriesForGroup(group) {
if (isSettingFavoritesGroup(group)) return getFavoriteSettingEntries();
+ const profile = getSettingProfileByGroup(group);
+ if (profile) return getProfileSettingEntries(profile);
return (SETTINGS?.items_by_group?.[group] || []).map((item) => ({ group, item }));
}
@@ -198,6 +282,8 @@ async function toggleSettingFavorite(name) {
function getSettingGroupParamNames(group) {
if (isSettingFavoritesGroup(group)) return getValidSettingFavoriteNames();
+ const profile = getSettingProfileByGroup(group);
+ if (profile) return getProfileSettingEntries(profile).map((entry) => entry.item.name).filter(Boolean);
const list = SETTINGS?.items_by_group?.[group] || [];
return list.map((item) => item.name).filter(Boolean);
}
@@ -241,6 +327,8 @@ function applyRestoredSettingValuesToRenderedItems(values) {
async function fetchSettingGroupValues(group, options = {}) {
if (!group) return {};
+ const profile = getSettingProfileByGroup(group);
+ if (profile) return { ...(profile.values || {}) };
const force = options.force === true;
const ttlMs = Number.isFinite(options.ttlMs) ? options.ttlMs : SETTING_VALUES_TTL_MS;
const names = getSettingGroupParamNames(group);
@@ -392,6 +480,7 @@ async function loadSettings(options = {}) {
if (SETTINGS && !force) {
await loadSettingFavorites();
+ await loadSettingProfiles();
renderGroups({ animateGroups: false });
renderSettingSubnav();
syncSettingSearchFabState();
@@ -413,6 +502,7 @@ async function loadSettings(options = {}) {
settingGroupValueCache.clear();
settingGroupValuePromises.clear();
await loadSettingFavorites(force);
+ await loadSettingProfiles(force);
rebuildSettingSearchEntries();
if (meta) {
@@ -464,10 +554,10 @@ function renderGroups(options = {}) {
const box = document.getElementById("groupList");
const animateGroups = options.animateGroups !== false;
const groups = getSettingGroupsForDisplay();
- const signature = groups.map((g) => `${g.group}:${g.count}`).join("|");
+ const signature = groups.map((g) => isSettingProfilesDivider(g) ? SETTING_PROFILES_DIVIDER : `${g.group}:${g.count ?? ""}:${g.label || ""}`).join("|");
function setGroupButtonLabel(button, label, count) {
- const text = `${label} (${count})`;
+ const text = Number.isFinite(Number(count)) ? `${label} (${count})` : label;
button.title = text;
button.innerHTML = `
${escapeHtml(text)}`;
requestAnimationFrame(() => {
@@ -482,9 +572,17 @@ function renderGroups(options = {}) {
if (!animateGroups && box.dataset.groupsSignature === signature && box.children.length === groups.length) {
Array.from(box.children).forEach((button, index) => {
const g = groups[index];
+ if (isSettingProfilesDivider(g)) {
+ button.className = "setting-profile-divider";
+ button.innerHTML = `
${escapeHtml(g.label || getSettingProfilesLabel())}`;
+ button.removeAttribute("data-group");
+ button.onclick = null;
+ return;
+ }
const label = getSettingGroupLabel(g.group);
button.className = "btn groupBtn";
if (isSettingFavoritesGroup(g.group)) button.classList.add("groupBtn--favorites");
+ if (isSettingProfileGroup(g.group)) button.classList.add("groupBtn--profile");
if (g.group === CURRENT_GROUP) button.classList.add("active");
button.dataset.group = g.group;
setGroupButtonLabel(button, label, g.count);
@@ -497,11 +595,21 @@ function renderGroups(options = {}) {
box.dataset.groupsSignature = signature;
groups.forEach(g => {
+ if (isSettingProfilesDivider(g)) {
+ const divider = document.createElement("div");
+ divider.className = animateGroups ? "setting-profile-divider ui-stagger-item" : "setting-profile-divider";
+ if (animateGroups) divider.style.setProperty("--i", String(box.children.length));
+ divider.innerHTML = `
${escapeHtml(g.label || getSettingProfilesLabel())}`;
+ box.appendChild(divider);
+ return;
+ }
+
const label = getSettingGroupLabel(g.group);
const b = document.createElement("button");
b.className = animateGroups ? "btn groupBtn ui-stagger-item" : "btn groupBtn";
if (isSettingFavoritesGroup(g.group)) b.classList.add("groupBtn--favorites");
+ if (isSettingProfileGroup(g.group)) b.classList.add("groupBtn--profile");
if (animateGroups) b.style.setProperty("--i", String(box.children.length));
if (g.group === CURRENT_GROUP) b.classList.add("active");
b.dataset.group = g.group;
@@ -520,12 +628,24 @@ function getSettingGroupMeta(group) {
virtual: true,
};
}
+ const profile = getSettingProfileByGroup(group);
+ if (profile) {
+ return {
+ group,
+ egroup: profile.name,
+ count: getProfileSettingEntries(profile).length,
+ profile,
+ virtual: true,
+ };
+ }
const groups = SETTINGS?.groups || [];
return groups.find((entry) => entry.group === group) || null;
}
function getSettingGroupLabel(group) {
if (isSettingFavoritesGroup(group)) return getSettingFavoritesLabel();
+ const profile = getSettingProfileByGroup(group);
+ if (profile) return profile.name;
const meta = getSettingGroupMeta(group);
if (!meta) return group;
if (LANG === "zh") return meta.cgroup || meta.egroup || meta.group;
@@ -540,7 +660,9 @@ let pendingSettingFocus = null;
let settingFocusClearTimer = null;
let settingSearchDebounceTimer = null;
let settingSearchEntries = [];
+let settingSearchScope = { type: "all", profileId: "" };
const settingPageRoot = document.getElementById("pageSetting");
+let settingFabMenuOpen = false;
function isCompactLandscapeMode() {
return window.matchMedia("(orientation: landscape)").matches;
@@ -557,7 +679,8 @@ function syncSettingSubnavFixedOffset() {
CURRENT_PAGE === "setting" &&
isFixedPortraitSettingSubnavMode() &&
screenItems.style.display !== "none" &&
- settingSubnavWrap.style.display !== "none";
+ settingSubnavWrap.style.display !== "none" &&
+ !settingPageRoot?.classList.contains("setting-profile-active");
if (!shouldFix) {
document.documentElement.style.removeProperty("--setting-fixed-subnav-height");
@@ -587,11 +710,386 @@ function syncSettingSearchFabState() {
const isOpen = Boolean(settingSearchPanel && !settingSearchPanel.hidden);
if (settingPageRoot) settingPageRoot.classList.toggle("setting-search-open", isOpen);
if (btnSettingSearch) {
- btnSettingSearch.classList.toggle("active", isOpen);
- btnSettingSearch.setAttribute("aria-expanded", isOpen ? "true" : "false");
+ btnSettingSearch.classList.toggle("active", isOpen || settingFabMenuOpen);
+ btnSettingSearch.setAttribute("aria-expanded", settingFabMenuOpen ? "true" : "false");
+ }
+}
+
+function normalizeSettingProfiles(profiles) {
+ return (Array.isArray(profiles) ? profiles : [])
+ .filter((profile) => profile && profile.id && profile.name && profile.values)
+ .map((profile) => ({
+ ...profile,
+ values: { ...(profile.values || {}) },
+ meta: { ...(profile.meta || {}) },
+ }));
+}
+
+async function loadSettingProfiles(force = false) {
+ if (!force && settingProfilesState.loaded) return settingProfilesState.profiles;
+ if (!force && settingProfilesState.loadPromise) return settingProfilesState.loadPromise;
+
+ settingProfilesState.loadPromise = getJson("/api/setting_profiles")
+ .then((payload) => {
+ settingProfilesState.loaded = true;
+ settingProfilesState.profiles = normalizeSettingProfiles(payload?.profiles || []);
+ return settingProfilesState.profiles;
+ })
+ .catch(() => {
+ settingProfilesState.loaded = true;
+ settingProfilesState.profiles = [];
+ return settingProfilesState.profiles;
+ })
+ .finally(() => {
+ settingProfilesState.loadPromise = null;
+ });
+
+ return settingProfilesState.loadPromise;
+}
+
+function updateSettingProfilesFromPayload(payload) {
+ if (!payload || !Array.isArray(payload.profiles)) return;
+ settingProfilesState.loaded = true;
+ settingProfilesState.profiles = normalizeSettingProfiles(payload.profiles);
+}
+
+function formatSettingProfileDate(value) {
+ const raw = String(value || "").trim();
+ if (!raw) return "";
+ const date = new Date(raw);
+ if (Number.isNaN(date.getTime())) return raw;
+ try {
+ return date.toLocaleString(LANG === "ko" ? "ko-KR" : undefined, {
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit",
+ hour12: false,
+ });
+ } catch {
+ return raw;
+ }
+}
+
+function settingProfileMetaRows(profile) {
+ const meta = profile?.meta || {};
+ const rows = [];
+ if (profile?.created_at) {
+ rows.push([getUIText("setting_profile_created", "Created"), settingsDiffEscape(formatSettingProfileDate(profile.created_at))]);
+ }
+ if (meta.branch) rows.push([getUIText("branch", "Branch"), settingsDiffEscape(meta.branch)]);
+ if (meta.commit) {
+ const commitText = meta.commit_short || String(meta.commit).slice(0, 7);
+ const commitValue = meta.commit_url
+ ? `
${settingsDiffEscape(commitText)}`
+ : settingsDiffEscape(commitText);
+ rows.push([getUIText("commit", "Commit"), commitValue]);
+ }
+ return rows;
+}
+
+async function openSettingProfileInfo(profile) {
+ const rows = settingProfileMetaRows(profile);
+ const messageHtml = rows.length
+ ? `
${rows.map(([label, value]) => `
+
+ ${settingsDiffEscape(label)}
+ ${value}
+
+ `).join("")}
`
+ : `
${settingsDiffEscape(getUIText("setting_profile_info_empty", "No profile metadata"))}
`;
+ await openAppDialog({
+ mode: "alert",
+ title: getUIText("setting_profile_info", "Profile Info"),
+ html: true,
+ messageHtml,
+ confirmLabel: getUIText("ok", "OK"),
+ });
+}
+
+async function saveSettingProfile(profileId, updates) {
+ const payload = await postJson("/api/setting_profiles/update", { id: profileId, ...(updates || {}) });
+ updateSettingProfilesFromPayload(payload);
+ return payload.profile || getSettingProfileById(profileId);
+}
+
+async function createSettingProfileFromCurrent() {
+ closeSettingFabMenu();
+ const name = await appPrompt(getUIText("setting_profile_create_prompt", "Enter a profile name."), {
+ title: getUIText("setting_profile_create_title", "Add Profile"),
+ placeholder: getUIText("setting_profile_name", "Profile name"),
+ });
+ if (!name || !String(name).trim()) return;
+
+ try {
+ const payload = await postJson("/api/setting_profiles", { name: String(name).trim() });
+ updateSettingProfilesFromPayload(payload);
+ const profile = payload.profile;
+ renderGroups({ animateGroups: false });
+ renderSettingSubnav();
+ if (profile?.id) {
+ await selectGroup(settingProfileGroup(profile.id));
+ showAppToast(getUIText("setting_profile_saved", "Profile saved"));
+ }
+ } catch (e) {
+ showAppToast(e?.message || getUIText("setting_profile_save_failed", "Failed to save profile"), { tone: "error" });
+ }
+}
+
+function setSettingProfileDialogClass(enabled) {
+ if (typeof appDialog !== "undefined" && appDialog) {
+ appDialog.classList.toggle("app-dialog--settings-diff", Boolean(enabled));
+ }
+}
+
+async function applySettingProfile(profile) {
+ if (!profile?.id) return;
+ let preview = null;
+ try {
+ const payload = await postJson("/api/setting_profiles/preview", { id: profile.id, values: profile.values || {} });
+ preview = payload.preview;
+ } catch (e) {
+ showAppToast(e?.message || getUIText("setting_profile_apply_failed", "Failed to preview profile"), { tone: "error" });
+ return;
+ }
+
+ const selected = typeof getSettingsDiffSelectedCount === "function" ? getSettingsDiffSelectedCount(preview) : 0;
+ const html = `
+
+
${settingsDiffEscape(profile.name)}
+ ${typeof renderSettingsDiffHtml === "function" ? renderSettingsDiffHtml(preview, {
+ nextLabel: getUIText("setting_profile_value", "Profile"),
+ }) : ""}
+
+ `;
+ const promise = openAppDialog({
+ mode: selected > 0 ? "confirm" : "alert",
+ title: getUIText("setting_profile_apply_title", "Apply Profile"),
+ html: true,
+ messageHtml: html,
+ confirmLabel: getUIText("apply", "Apply"),
+ cancelLabel: getUIText("cancel", "Cancel"),
+ });
+ setSettingProfileDialogClass(true);
+ const ok = await promise.finally(() => setSettingProfileDialogClass(false));
+ if (selected <= 0 || !ok) return;
+
+ try {
+ const result = await postJson("/api/setting_profiles/apply", { id: profile.id, values: profile.values || {} });
+ const failed = new Set((result.result?.fails || []).map((entry) => String(entry?.key || "")).filter(Boolean));
+ const restoredValues = {};
+ (result.preview?.entries || []).forEach((entry) => {
+ if (!entry?.apply || failed.has(String(entry.key))) return;
+ restoredValues[entry.key] = entry.value;
+ });
+ if (Object.keys(restoredValues).length) {
+ window.dispatchEvent(new CustomEvent("carrot:paramsrestored", {
+ detail: { source: "setting_profile", values: restoredValues },
+ }));
+ Object.entries(restoredValues).forEach(([name, value]) => {
+ window.dispatchEvent(new CustomEvent("carrot:paramchange", {
+ detail: { name, value, source: "setting_profile" },
+ }));
+ });
+ }
+ showAppToast(getUIText("setting_profile_apply_done", "Profile applied"));
+ } catch (e) {
+ showAppToast(e?.message || getUIText("setting_profile_apply_failed", "Failed to apply profile"), { tone: "error" });
+ }
+}
+
+async function deleteSettingProfile(profile) {
+ if (!profile?.id) return;
+ const ok = await appConfirm(getUIText("setting_profile_delete_confirm", "Delete this profile?\n{name}", { name: profile.name }), {
+ title: getUIText("setting_profile_delete", "Delete Profile"),
+ confirmLabel: getUIText("delete", "Delete"),
+ });
+ if (!ok) return;
+
+ try {
+ const payload = await postJson("/api/setting_profiles/delete", { id: profile.id });
+ updateSettingProfilesFromPayload(payload);
+ CURRENT_GROUP = null;
+ renderGroups({ animateGroups: false });
+ renderSettingSubnav();
+ showSettingScreen("groups", false);
+ showAppToast(getUIText("setting_profile_deleted", "Profile deleted"));
+ } catch (e) {
+ showAppToast(e?.message || getUIText("setting_profile_save_failed", "Failed to save profile"), { tone: "error" });
+ }
+}
+
+function closeSettingProfileActionMenus(exceptPanel = null) {
+ document.querySelectorAll(".setting-profile-action-menu.is-open").forEach((menu) => {
+ if (exceptPanel && menu === exceptPanel) return;
+ menu.classList.remove("is-open");
+ const button = menu.querySelector(".setting-profile-action-menu__button");
+ const panel = menu.querySelector(".setting-profile-action-menu__panel");
+ if (button) button.setAttribute("aria-expanded", "false");
+ if (panel) {
+ panel.hidden = true;
+ panel.setAttribute("aria-hidden", "true");
+ }
+ });
+}
+
+function makeSettingProfileMenuItem(label, onClick, className = "") {
+ const button = document.createElement("button");
+ button.type = "button";
+ button.className = `setting-profile-action-menu__item ui-dropdown-menu__item${className ? ` ${className}` : ""}`;
+ button.setAttribute("role", "menuitem");
+ button.textContent = label;
+ button.onclick = (event) => {
+ event.stopPropagation();
+ closeSettingProfileActionMenus();
+ onClick();
+ };
+ return button;
+}
+
+function appendSettingProfileHeader(profile, container) {
+ const panel = document.createElement("div");
+ panel.className = "setting-profile-panel";
+
+ const titleRow = document.createElement("div");
+ titleRow.className = "setting-profile-panel__titleRow";
+ const input = document.createElement("input");
+ input.className = "setting-profile-panel__name";
+ input.type = "text";
+ input.maxLength = 40;
+ input.value = profile.name || "";
+ input.setAttribute("aria-label", getUIText("setting_profile_name", "Profile name"));
+ let nameSaveTimer = 0;
+ let nameSaveInFlight = null;
+ async function persistProfileName() {
+ const nextName = input.value.trim();
+ if (!nextName) {
+ input.value = profile.name || "";
+ return;
+ }
+ if (nextName === profile.name) return;
+ if (nameSaveInFlight) {
+ try { await nameSaveInFlight; } catch {}
+ if (nextName === profile.name) return;
+ }
+ try {
+ input.classList.add("is-saving");
+ nameSaveInFlight = saveSettingProfile(profile.id, { name: nextName });
+ const nextProfile = await nameSaveInFlight;
+ if (nextProfile) profile.name = nextProfile.name;
+ if (itemsTitle) itemsTitle.textContent = profile.name;
+ renderGroups({ animateGroups: false });
+ renderSettingSubnav();
+ } catch (e) {
+ showAppToast(e?.message || getUIText("setting_profile_save_failed", "Failed to save profile"), { tone: "error" });
+ } finally {
+ nameSaveInFlight = null;
+ input.classList.remove("is-saving");
+ }
+ }
+ function scheduleProfileNameSave(delay = 500) {
+ if (nameSaveTimer) clearTimeout(nameSaveTimer);
+ nameSaveTimer = window.setTimeout(() => {
+ nameSaveTimer = 0;
+ persistProfileName().catch(() => {});
+ }, delay);
+ }
+ input.addEventListener("input", () => scheduleProfileNameSave());
+ input.addEventListener("blur", () => {
+ if (nameSaveTimer) {
+ clearTimeout(nameSaveTimer);
+ nameSaveTimer = 0;
+ }
+ persistProfileName().catch(() => {});
+ });
+ input.addEventListener("keydown", (event) => {
+ if (event.key !== "Enter") return;
+ event.preventDefault();
+ if (nameSaveTimer) {
+ clearTimeout(nameSaveTimer);
+ nameSaveTimer = 0;
+ }
+ persistProfileName().then(() => input.blur()).catch(() => {});
+ });
+ const menu = document.createElement("div");
+ menu.className = "setting-profile-action-menu ui-dropdown-menu";
+ const menuBtn = document.createElement("button");
+ menuBtn.type = "button";
+ menuBtn.className = "setting-profile-action-menu__button ui-dropdown-menu__button";
+ menuBtn.setAttribute("aria-haspopup", "menu");
+ menuBtn.setAttribute("aria-expanded", "false");
+ menuBtn.setAttribute("aria-label", getUIText("setting_profile_menu", "Profile menu"));
+ menuBtn.innerHTML = `
+
+ `;
+ const menuPanel = document.createElement("div");
+ menuPanel.className = "setting-profile-action-menu__panel ui-dropdown-menu__panel";
+ menuPanel.setAttribute("role", "menu");
+ menuPanel.setAttribute("aria-hidden", "true");
+ menuPanel.hidden = true;
+ menuPanel.appendChild(makeSettingProfileMenuItem(
+ getUIText("setting_profile_search", "Search Profile"),
+ () => openSettingSearchPanel({ scope: { type: "profile", profileId: profile.id } }).catch(() => {}),
+ ));
+ menuPanel.appendChild(makeSettingProfileMenuItem(
+ getUIText("setting_profile_info", "Info"),
+ () => openSettingProfileInfo(profile),
+ ));
+ menuPanel.appendChild(makeSettingProfileMenuItem(
+ getUIText("apply", "Apply"),
+ () => applySettingProfile(profile),
+ "setting-profile-action-menu__item--primary",
+ ));
+ menuPanel.appendChild(makeSettingProfileMenuItem(
+ getUIText("delete", "Delete"),
+ () => deleteSettingProfile(profile),
+ "setting-profile-action-menu__item--danger",
+ ));
+ menuBtn.onclick = (event) => {
+ event.stopPropagation();
+ const nextOpen = !menu.classList.contains("is-open");
+ closeSettingProfileActionMenus(menu);
+ menu.classList.toggle("is-open", nextOpen);
+ menuBtn.setAttribute("aria-expanded", nextOpen ? "true" : "false");
+ menuPanel.hidden = !nextOpen;
+ menuPanel.setAttribute("aria-hidden", nextOpen ? "false" : "true");
+ };
+ menu.appendChild(menuBtn);
+ menu.appendChild(menuPanel);
+ titleRow.appendChild(input);
+ titleRow.appendChild(menu);
+
+ panel.appendChild(titleRow);
+ container.appendChild(panel);
+}
+
+function syncSettingFabMenuState() {
+ if (settingFabMenu) settingFabMenu.classList.toggle("is-open", settingFabMenuOpen);
+ if (settingFabActions) {
+ settingFabActions.hidden = !settingFabMenuOpen;
+ settingFabActions.setAttribute("aria-hidden", settingFabMenuOpen ? "false" : "true");
+ }
+ if (btnSettingSearch) {
+ btnSettingSearch.classList.toggle("active", settingFabMenuOpen || Boolean(settingSearchPanel && !settingSearchPanel.hidden));
+ btnSettingSearch.setAttribute("aria-expanded", settingFabMenuOpen ? "true" : "false");
}
}
+function closeSettingFabMenu() {
+ if (!settingFabMenuOpen) return;
+ settingFabMenuOpen = false;
+ syncSettingFabMenuState();
+}
+
+function toggleSettingFabMenu() {
+ settingFabMenuOpen = !settingFabMenuOpen;
+ syncSettingFabMenuState();
+}
+
function mountSettingSearchOverlay() {
if (settingSearchBackdrop && settingSearchBackdrop.parentElement !== document.body) {
document.body.appendChild(settingSearchBackdrop);
@@ -601,6 +1099,35 @@ function mountSettingSearchOverlay() {
}
}
+function makeSettingSearchEntry({ source, profile = null, group, item }) {
+ const groupLabel = getSettingGroupLabel(group);
+ const title = formatItemText(item, "title", "etitle", "");
+ const descr = formatItemText(item, "descr", "edescr", "");
+ const isProfile = source === "profile" && profile?.id;
+ const profileName = isProfile ? String(profile.name || "") : "";
+ const sourceLabel = isProfile
+ ? getUIText("setting_search_source_profile", "Profile")
+ : getUIText("setting_search_source_carrot", "CarrotPilot");
+ const contextLabel = isProfile
+ ? `${profileName} / ${groupLabel}`
+ : groupLabel;
+
+ return {
+ source: isProfile ? "profile" : "carrot",
+ sourceLabel,
+ profileId: isProfile ? profile.id : "",
+ profileName,
+ group: isProfile ? settingProfileGroup(profile.id) : group,
+ originalGroup: group,
+ groupLabel,
+ contextLabel,
+ name: item.name,
+ title,
+ descr,
+ haystack: [sourceLabel, profileName, groupLabel, item.name, title, descr].join("\n").toLowerCase(),
+ };
+}
+
function rebuildSettingSearchEntries() {
const groups = SETTINGS?.groups || [];
const entries = [];
@@ -611,16 +1138,18 @@ function rebuildSettingSearchEntries() {
const list = SETTINGS?.items_by_group?.[group] || [];
list.forEach((item) => {
- const title = formatItemText(item, "title", "etitle", "");
- const descr = formatItemText(item, "descr", "edescr", "");
- entries.push({
- group,
- groupLabel,
- name: item.name,
- title,
- descr,
- haystack: [groupLabel, item.name, title, descr].join("\n").toLowerCase(),
- });
+ entries.push(makeSettingSearchEntry({ source: "carrot", group, item }));
+ });
+ });
+
+ (settingProfilesState.profiles || []).forEach((profile) => {
+ getProfileSettingEntries(profile).forEach((entry) => {
+ entries.push(makeSettingSearchEntry({
+ source: "profile",
+ profile,
+ group: entry.group,
+ item: entry.item,
+ }));
});
});
@@ -645,6 +1174,14 @@ function highlightSettingSearchText(text, query) {
return `${escapeHtml(raw.slice(0, start))}
${escapeHtml(raw.slice(start, end))}${escapeHtml(raw.slice(end))}`;
}
+function getSettingSearchScopeLabel() {
+ if (settingSearchScope.type === "profile") {
+ const profile = getSettingProfileById(settingSearchScope.profileId);
+ return profile?.name || getUIText("setting_search_source_profile", "Profile");
+ }
+ return getUIText("setting_search_all", "All settings");
+}
+
function clearSettingItemFocus() {
if (settingFocusClearTimer) {
clearTimeout(settingFocusClearTimer);
@@ -779,6 +1316,12 @@ function focusSettingItem(name, behavior = "smooth") {
);
if (!target) return false;
+ const section = target.closest(".setting-profile-section");
+ if (section?.classList.contains("is-collapsed")) {
+ section.classList.remove("is-collapsed");
+ section.querySelector(".setting-profile-section__header")?.setAttribute("aria-expanded", "true");
+ }
+
clearSettingItemFocus();
target.classList.add("is-focus-hit");
target.scrollIntoView({ behavior, block: "center" });
@@ -793,7 +1336,6 @@ function focusSettingItem(name, behavior = "smooth") {
}
function closeSettingSearchPanel(options = {}) {
- const clear = Boolean(options.clear);
const syncHistory = Boolean(options.syncHistory);
const fromHistory = Boolean(options.fromHistory);
if (settingSearchDebounceTimer) {
@@ -807,8 +1349,13 @@ function closeSettingSearchPanel(options = {}) {
if (settingSearchBackdrop) settingSearchBackdrop.hidden = true;
syncSettingSearchFabState();
- if (clear && settingSearchInput) settingSearchInput.value = "";
- if (clear && settingSearchResults) settingSearchResults.innerHTML = "";
+ if (settingSearchInput) {
+ settingSearchInput.value = "";
+ settingSearchInput.placeholder = getUIText("setting_search_placeholder", "Search name, description, group");
+ settingSearchInput.removeAttribute("aria-label");
+ }
+ if (settingSearchResults) settingSearchResults.innerHTML = "";
+ settingSearchScope = { type: "all", profileId: "" };
syncModalBodyLock();
const state = history.state || {};
@@ -842,8 +1389,14 @@ function renderSettingSearchResults(query = "") {
const q = trimmed.toLowerCase();
const matches = getSettingSearchEntries()
- .filter((entry) => entry.haystack.includes(q))
- .slice(0, 24);
+ .filter((entry) => {
+ if (!entry.haystack.includes(q)) return false;
+ if (settingSearchScope.type === "profile") {
+ return entry.source === "profile" && entry.profileId === settingSearchScope.profileId;
+ }
+ return true;
+ })
+ .slice(0, 36);
settingSearchResults.innerHTML = "";
@@ -855,36 +1408,70 @@ function renderSettingSearchResults(query = "") {
return;
}
- matches.forEach((entry) => {
- const button = document.createElement("button");
- button.type = "button";
- button.className = "setting-search-result";
- button.innerHTML = `
-
${highlightSettingSearchText(entry.groupLabel, trimmed)}
-
${highlightSettingSearchText(entry.title || entry.name, trimmed)}
- ${entry.name && entry.name !== entry.title ? `
${highlightSettingSearchText(entry.name, trimmed)}
` : ""}
- ${entry.descr ? `
${highlightSettingSearchText(entry.descr, trimmed)}
` : ""}
+ const sections = [
+ {
+ key: "carrot",
+ title: getUIText("setting_search_source_carrot", "CarrotPilot"),
+ entries: matches.filter((entry) => entry.source === "carrot"),
+ },
+ {
+ key: "profile",
+ title: getUIText("setting_search_source_profile", "Profile"),
+ entries: matches.filter((entry) => entry.source === "profile"),
+ },
+ ].filter((section) => section.entries.length);
+
+ sections.forEach((section) => {
+ const sectionEl = document.createElement("div");
+ sectionEl.className = "setting-search-section";
+ sectionEl.innerHTML = `
+
+ ${escapeHtml(section.title)}
+ ${section.entries.length}
+
`;
- button.onclick = async () => {
- try {
- pendingSettingFocus = { group: entry.group, name: entry.name };
- closeSettingSearchPanel({ syncHistory: false });
- if (CURRENT_GROUP === entry.group && screenItems && screenItems.style.display !== "none") {
- focusSettingItem(entry.name);
- return;
+ settingSearchResults.appendChild(sectionEl);
+
+ section.entries.forEach((entry) => {
+ const button = document.createElement("button");
+ button.type = "button";
+ button.className = "setting-search-result";
+ const metaLabel = entry.source === "profile"
+ ? `${entry.profileName} / ${entry.groupLabel}`
+ : entry.groupLabel;
+ button.innerHTML = `
+
${highlightSettingSearchText(metaLabel, trimmed)}
+
${highlightSettingSearchText(entry.title || entry.name, trimmed)}
+ ${entry.name && entry.name !== entry.title ? `
${highlightSettingSearchText(entry.name, trimmed)}
` : ""}
+ ${entry.descr ? `
${highlightSettingSearchText(entry.descr, trimmed)}
` : ""}
+ `;
+ button.onclick = async () => {
+ try {
+ pendingSettingFocus = { group: entry.group, name: entry.name };
+ if (entry.source === "profile" && entry.profileId && entry.originalGroup) {
+ settingProfileSectionExpandedState.set(`${entry.profileId}:${entry.originalGroup}`, true);
+ }
+ closeSettingSearchPanel({ syncHistory: false });
+ if (CURRENT_GROUP === entry.group && screenItems && screenItems.style.display !== "none") {
+ focusSettingItem(entry.name);
+ return;
+ }
+ await activateSettingGroup(entry.group, true);
+ } catch (e) {
+ showAppToast(e.message || "Search jump failed", { tone: "error" });
}
- await activateSettingGroup(entry.group, true);
- } catch (e) {
- showAppToast(e.message || "Search jump failed", { tone: "error" });
- }
- };
- settingSearchResults.appendChild(button);
+ };
+ sectionEl.appendChild(button);
+ });
});
}
async function openSettingSearchPanel(options = {}) {
const pushHistory = options.pushHistory !== false;
+ const scope = options.scope || { type: "all", profileId: "" };
if (CURRENT_PAGE !== "setting") return;
+ closeSettingFabMenu();
+ closeSettingProfileActionMenus();
if (!SETTINGS) {
try {
await loadSettings();
@@ -892,6 +1479,12 @@ async function openSettingSearchPanel(options = {}) {
// no-op
}
}
+ await loadSettingProfiles();
+ settingSearchScope = {
+ type: scope.type === "profile" && scope.profileId ? "profile" : "all",
+ profileId: scope.type === "profile" && scope.profileId ? String(scope.profileId) : "",
+ };
+ rebuildSettingSearchEntries();
if (!settingSearchPanel) return;
mountSettingSearchOverlay();
settingSearchPanel.hidden = false;
@@ -905,9 +1498,17 @@ async function openSettingSearchPanel(options = {}) {
screen: (screenItems && screenItems.style.display !== "none") ? "items" : "groups",
group: CURRENT_GROUP || null,
search: true,
+ searchScope: settingSearchScope.type,
+ profileId: settingSearchScope.profileId || null,
}, "");
}
syncModalBodyLock();
+ if (settingSearchInput) {
+ settingSearchInput.placeholder = settingSearchScope.type === "profile"
+ ? getUIText("setting_profile_search_placeholder", "Search in this profile")
+ : getUIText("setting_search_placeholder", "Search name, description, group");
+ settingSearchInput.setAttribute("aria-label", getSettingSearchScopeLabel());
+ }
renderSettingSearchResults(settingSearchInput?.value || "");
requestAnimationFrame(() => {
settingSearchInput?.focus({ preventScroll: true });
@@ -924,7 +1525,60 @@ function toggleSettingSearchPanel() {
}
if (btnSettingSearch) {
- btnSettingSearch.onclick = () => toggleSettingSearchPanel();
+ btnSettingSearch.onclick = () => toggleSettingFabMenu();
+}
+
+if (btnSettingFabSearch) {
+ btnSettingFabSearch.onclick = () => {
+ closeSettingFabMenu();
+ openSettingSearchPanel().catch(() => {});
+ };
+}
+
+if (btnSettingFabProfileAdd) {
+ btnSettingFabProfileAdd.onclick = () => {
+ createSettingProfileFromCurrent().catch(() => {});
+ };
+}
+
+if (btnSettingFabResetDefaults) {
+ btnSettingFabResetDefaults.onclick = async () => {
+ closeSettingFabMenu();
+ const ok = await appConfirm(getUIText(
+ "setting_reset_defaults_confirm",
+ "Reset all settings to defaults?"
+ ), {
+ title: getUIText("setting_reset_defaults", "Reset Settings"),
+ confirmLabel: getUIText("ok", "OK"),
+ });
+ if (!ok) return;
+
+ btnSettingFabResetDefaults.disabled = true;
+ try {
+ const payload = await postJson("/api/set_default", {});
+ if (!payload?.ok) {
+ throw new Error(payload?.error || getUIText("setting_reset_defaults_failed", "Settings reset failed"));
+ }
+ if (payload.values && typeof payload.values === "object") {
+ window.dispatchEvent(new CustomEvent("carrot:paramsrestored", {
+ detail: { source: "setting_reset_defaults", values: payload.values },
+ }));
+ Object.entries(payload.values).forEach(([name, value]) => {
+ window.dispatchEvent(new CustomEvent("carrot:paramchange", {
+ detail: { name, value, source: "setting_reset_defaults" },
+ }));
+ });
+ }
+ showAppToast(getUIText(
+ "setting_reset_defaults_done",
+ payload.message || "Settings reset complete"
+ ));
+ } catch (e) {
+ showAppToast(getUIText("setting_reset_defaults_failed", "Settings reset failed"), { tone: "error" });
+ } finally {
+ btnSettingFabResetDefaults.disabled = false;
+ }
+ };
}
if (settingSearchBackdrop) {
@@ -952,6 +1606,29 @@ if (settingSearchInput) {
window.addEventListener("keydown", (e) => {
if (e.key === "Escape" && settingSearchPanel && !settingSearchPanel.hidden) {
closeSettingSearchPanel({ syncHistory: true });
+ return;
+ }
+ if (e.key === "Escape") {
+ closeSettingProfileActionMenus();
+ }
+ if (e.key === "Escape" && settingFabMenuOpen) {
+ closeSettingFabMenu();
+ }
+});
+
+document.addEventListener("pointerdown", (e) => {
+ if (!(e.target instanceof Element && e.target.closest(".setting-profile-action-menu"))) {
+ closeSettingProfileActionMenus();
+ }
+ if (!settingFabMenuOpen || !settingFabMenu) return;
+ if (settingFabMenu.contains(e.target)) return;
+ closeSettingFabMenu();
+});
+
+window.addEventListener("carrot:pagechange", (event) => {
+ if (event?.detail?.page !== "setting") {
+ closeSettingFabMenu();
+ closeSettingProfileActionMenus();
}
});
@@ -968,7 +1645,11 @@ function updateSettingSubnavLayoutState() {
}
function getSettingSubnavGroups() {
- return getSettingGroupsForDisplay();
+ return getSettingGroupsForDisplay().filter((entry) =>
+ !isSettingProfilesDivider(entry) &&
+ !isSettingFavoritesGroup(entry.group) &&
+ !isSettingProfileGroup(entry.group)
+ );
}
function getSettingSubnavGroupIndex(group = CURRENT_GROUP) {
@@ -1195,6 +1876,7 @@ function renderSettingSubnav() {
const entry = groups[index];
button.className = "setting-subnav__tab";
if (isSettingFavoritesGroup(entry.group)) button.classList.add("setting-subnav__tab--favorites");
+ if (isSettingProfileGroup(entry.group)) button.classList.add("setting-subnav__tab--profile");
if (entry.group === CURRENT_GROUP) button.classList.add("is-active");
button.dataset.group = entry.group;
button.textContent = getSettingGroupLabel(entry.group);
@@ -1212,6 +1894,7 @@ function renderSettingSubnav() {
const button = document.createElement("button");
button.className = "setting-subnav__tab";
if (isSettingFavoritesGroup(entry.group)) button.classList.add("setting-subnav__tab--favorites");
+ if (isSettingProfileGroup(entry.group)) button.classList.add("setting-subnav__tab--profile");
if (entry.group === CURRENT_GROUP) button.classList.add("is-active");
button.dataset.group = entry.group;
button.textContent = getSettingGroupLabel(entry.group);
@@ -1366,7 +2049,7 @@ if (settingSubnavWrap) {
function selectGroup(group, pushHistory = true) {
const shouldPush = pushHistory && !(isCompactLandscapeMode() && CURRENT_PAGE === "setting");
const options = (isCompactLandscapeMode() && CURRENT_PAGE === "setting")
- ? { animateItems: false, animateGroups: false }
+ ? { animateGroups: false }
: {};
activateSettingGroup(group, shouldPush, options).catch((e) => console.log("[Setting] selectGroup failed:", e));
}
@@ -1385,6 +2068,8 @@ async function renderItems(group, options = {}) {
const entries = getSettingItemEntriesForGroup(group);
const list = entries.map((entry) => entry.item);
+ const profile = getSettingProfileByGroup(group);
+ if (screenItems) screenItems.classList.toggle("setting-screen-items--profile", Boolean(profile));
if (meta) meta.textContent = `${group} / ${list.length}`;
const groupLabel = getSettingGroupLabel(group);
settingTitle.textContent = (UI_STRINGS[LANG].setting || "Setting") + " - " + groupLabel;
@@ -1424,11 +2109,61 @@ async function renderItems(group, options = {}) {
return;
}
+ if (profile) appendSettingProfileHeader(profile, itemsBox);
+
+ const profileSectionCounts = new Map();
+ if (profile) {
+ entries.forEach((entry) => {
+ profileSectionCounts.set(entry.group, (profileSectionCounts.get(entry.group) || 0) + 1);
+ });
+ }
+ let lastProfileGroup = "";
+ let currentProfileSectionBody = null;
list.forEach((p, index) => {
const name = p.name;
const originGroup = entries[index]?.group || group;
if (!(name in UNIT_INDEX)) UNIT_INDEX[name] = 0;
+ if (profile && originGroup !== lastProfileGroup) {
+ lastProfileGroup = originGroup;
+ const section = document.createElement("div");
+ section.className = animateItems ? "setting-profile-section ui-stagger-item" : "setting-profile-section";
+ if (animateItems) section.style.setProperty("--i", String(Math.min(index + 1, 14)));
+ const stateKey = `${profile.id}:${originGroup}`;
+ const expanded = settingProfileSectionExpandedState.has(stateKey)
+ ? settingProfileSectionExpandedState.get(stateKey)
+ : true;
+ const sectionLabel = getSettingGroupLabel(originGroup);
+ const sectionCount = profileSectionCounts.get(originGroup) || 0;
+ section.classList.toggle("is-collapsed", !expanded);
+
+ const header = document.createElement("button");
+ header.type = "button";
+ header.className = "setting-profile-section__header";
+ header.setAttribute("aria-expanded", expanded ? "true" : "false");
+ header.innerHTML = `
+
${settingsDiffEscape(sectionLabel)}
+
${settingsDiffEscape(sectionCount)}
+
+ `;
+ const body = document.createElement("div");
+ body.className = "setting-profile-section__body";
+ const bodyInner = document.createElement("div");
+ bodyInner.className = "setting-profile-section__bodyInner";
+ header.onclick = () => {
+ const nextExpanded = section.classList.toggle("is-collapsed") ? false : true;
+ settingProfileSectionExpandedState.set(stateKey, nextExpanded);
+ header.setAttribute("aria-expanded", nextExpanded ? "true" : "false");
+ };
+ body.appendChild(bodyInner);
+ section.appendChild(header);
+ section.appendChild(body);
+ itemsBox.appendChild(section);
+ currentProfileSectionBody = bodyInner;
+ }
+
const title = formatItemText(p, "title", "etitle", "");
const descr = formatItemText(p, "descr", "edescr", "");
@@ -1497,7 +2232,7 @@ async function renderItems(group, options = {}) {
el.appendChild(top);
el.appendChild(d);
- itemsBox.appendChild(el);
+ (currentProfileSectionBody || itemsBox).appendChild(el);
const cur = (name in values) ? values[name] : p.default;
val.textContent = String(cur);
@@ -1520,10 +2255,22 @@ async function renderItems(group, options = {}) {
async function commitSettingValue(next) {
try {
- await setParam(name, next);
+ if (profile) {
+ const nextValues = { ...(profile.values || {}), [name]: next };
+ const nextProfile = await saveSettingProfile(profile.id, { values: nextValues });
+ if (nextProfile) {
+ profile.values = { ...(nextProfile.values || nextValues) };
+ } else {
+ profile.values = nextValues;
+ }
+ } else {
+ await setParam(name, next);
+ }
val.textContent = String(next);
- cacheSettingValue(name, next, group);
- if (originGroup !== group) cacheSettingValue(name, next, originGroup);
+ if (!profile) {
+ cacheSettingValue(name, next, group);
+ if (originGroup !== group) cacheSettingValue(name, next, originGroup);
+ }
} catch (e) {
showAppToast((UI_STRINGS[LANG].set_failed || "set failed: ") + e.message, { tone: "error" });
}
@@ -1540,10 +2287,36 @@ async function renderItems(group, options = {}) {
title: getUIText("setting_value_title", "Edit value"),
defaultValue: val.textContent,
placeholder: String(p.default),
+ confirmLabel: getUIText("ok", "OK"),
+ showCancel: false,
+ defaultActionLabel: getUIText("default_value", "Default"),
+ defaultActionValue: { settingDefaultAction: true, value: String(p.default) },
}
);
if (input === null) return;
+ if (input?.settingDefaultAction) {
+ const defaultValue = input.value;
+ const ok = await appConfirm(getUIText(
+ "default_value_confirm",
+ "Restore {name} to default value ({value})?",
+ { name, value: defaultValue }
+ ), {
+ title: getUIText("default_value", "Default"),
+ confirmLabel: getUIText("ok", "OK"),
+ });
+ if (!ok) return;
+
+ const nextDefault = normalizeSettingValue(defaultValue);
+ if (nextDefault === null) {
+ showAppToast(getUIText("setting_value_invalid", "Enter a valid number."), { tone: "error" });
+ return;
+ }
+ if (String(nextDefault) === String(val.textContent)) return;
+ await commitSettingValue(nextDefault);
+ return;
+ }
+
const next = normalizeSettingValue(input);
if (next === null) {
showAppToast(getUIText("setting_value_invalid", "Enter a valid number."), { tone: "error" });
@@ -1656,7 +2429,7 @@ async function syncSettingViewportLayout(options = {}) {
if (CURRENT_PAGE !== "setting" || !SETTINGS) return;
settingViewportLayoutSignature = getSettingViewportLayoutSignature();
const animateChrome = options.animateChrome === true;
- const animateItems = options.animateItems === true;
+ const animateItems = options.animateItems === true || animateChrome;
const splitLandscape = isCompactLandscapeMode();
if (typeof syncSettingSplitLayoutClass === "function") {
syncSettingSplitLayoutClass(splitLandscape);
@@ -1668,7 +2441,7 @@ async function syncSettingViewportLayout(options = {}) {
showSettingScreen("items", false);
}
if (typeof renderDeviceTab === "function") {
- await renderDeviceTab();
+ await renderDeviceTab({ animateGroups: animateChrome, animateItems });
}
if (!splitLandscape) {
const deviceItemsEl = document.getElementById("deviceItems");
diff --git a/selfdrive/carrot/web/js/pages/setting_device.js b/selfdrive/carrot/web/js/pages/setting_device.js
index f349792039..f7a7a42dfb 100644
--- a/selfdrive/carrot/web/js/pages/setting_device.js
+++ b/selfdrive/carrot/web/js/pages/setting_device.js
@@ -135,10 +135,11 @@ async function loadDeviceNetwork(useCache = true) {
return deviceNetworkLoadPromise;
}
-function renderDeviceGroups() {
+function renderDeviceGroups(options = {}) {
const groupContainer = document.getElementById("deviceGroupList");
const subnavContainer = document.getElementById("deviceSubnav");
if (!groupContainer) return;
+ const animateGroups = options.animateGroups !== false;
groupContainer.innerHTML = "";
if (subnavContainer) subnavContainer.innerHTML = "";
@@ -148,11 +149,12 @@ function renderDeviceGroups() {
CURRENT_DEVICE_GROUP = visibleGroups[0]?.id || "Device";
}
- visibleGroups.forEach((group) => {
+ visibleGroups.forEach((group, index) => {
const label = getDeviceGroupLabel(group.id);
const button = document.createElement("button");
button.type = "button";
- button.className = "btn groupBtn";
+ button.className = animateGroups ? "btn groupBtn ui-stagger-item" : "btn groupBtn";
+ if (animateGroups) button.style.setProperty("--i", String(index));
if (group.id === CURRENT_DEVICE_GROUP) button.classList.add("active");
button.dataset.deviceGroup = group.id;
button.innerHTML = `
${escapeHtml(label)}`;
@@ -162,7 +164,8 @@ function renderDeviceGroups() {
if (subnavContainer) {
const tab = document.createElement("button");
tab.type = "button";
- tab.className = "setting-subnav__tab";
+ tab.className = animateGroups ? "setting-subnav__tab ui-stagger-item" : "setting-subnav__tab";
+ if (animateGroups) tab.style.setProperty("--i", String(index));
if (group.id === CURRENT_DEVICE_GROUP) tab.classList.add("is-active");
tab.dataset.deviceGroup = group.id;
tab.textContent = label;
@@ -172,17 +175,28 @@ function renderDeviceGroups() {
});
}
-async function renderDeviceTab() {
+function applyDeviceItemsStagger(container) {
+ if (!container) return;
+ Array.from(container.children).forEach((child, index) => {
+ if (!child.classList?.contains("setting")) return;
+ child.classList.add("ui-stagger-item");
+ child.style.setProperty("--i", String(index));
+ });
+}
+
+async function renderDeviceTab(options = {}) {
syncSettingTabState("device");
- renderDeviceGroups();
+ const animateGroups = options.animateGroups !== false;
+ const animateItems = options.animateItems !== false;
+ renderDeviceGroups({ animateGroups });
if (!deviceTabLoaded) {
deviceTabLoaded = true;
loadDeviceParams("Device", true).then(() => {
- if (CURRENT_SETTING_TAB === "device") renderDeviceGroups();
+ if (CURRENT_SETTING_TAB === "device") renderDeviceGroups({ animateGroups: false });
});
}
if (typeof isCompactLandscapeMode === "function" && isCompactLandscapeMode()) {
- await renderDeviceItems(CURRENT_DEVICE_GROUP, false);
+ await renderDeviceItems(CURRENT_DEVICE_GROUP, false, { animateItems });
}
}
@@ -190,7 +204,7 @@ async function selectDeviceGroup(groupId) {
CURRENT_DEVICE_GROUP = groupId || CURRENT_DEVICE_GROUP;
renderDeviceGroups();
syncSettingTabState("device");
- await renderDeviceItems(CURRENT_DEVICE_GROUP, true);
+ await renderDeviceItems(CURRENT_DEVICE_GROUP, true, { animateItems: true });
}
async function getDeviceGroupValues(groupId) {
@@ -223,6 +237,9 @@ async function renderDeviceItems(groupId, showItemsScreen = true, options = {})
}
itemsContainer.innerHTML = renderDeviceGroupItems(groupId, values) || `
-
`;
+ if (!silentRefresh && options.animateItems !== false) {
+ applyDeviceItemsStagger(itemsContainer);
+ }
bindDeviceTabEvents(itemsContainer);
syncDeviceGroupActiveState(groupId);
syncDeviceNetworkRefresh();
diff --git a/selfdrive/carrot/web/js/pages/setting_device_config.js b/selfdrive/carrot/web/js/pages/setting_device_config.js
index 0923b3055c..ac61ece192 100644
--- a/selfdrive/carrot/web/js/pages/setting_device_config.js
+++ b/selfdrive/carrot/web/js/pages/setting_device_config.js
@@ -31,21 +31,11 @@ const DEVICE_SOFTWARE_PARAMS = [
"UpdaterNewDescription",
];
-const DEVICE_LANGUAGES = [
+const DEVICE_LANGUAGES = window.CarrotDeviceLanguageOptions || [
{ code: "main_en", name: "English" },
{ code: "main_ko", name: "한국어" },
{ code: "main_zh-CHS", name: "简体中文" },
{ code: "main_zh-CHT", name: "繁體中文" },
- { code: "main_ja", name: "日本語" },
- { code: "main_fr", name: "Français" },
- { code: "main_pt-BR", name: "Português" },
- { code: "main_de", name: "Deutsch" },
- { code: "main_es", name: "Español" },
- { code: "main_tr", name: "Türkçe" },
- { code: "main_th", name: "ไทย" },
- { code: "main_ar", name: "العربية" },
- { code: "main_pl", name: "Polski" },
- { code: "main_nl", name: "Nederlands" },
];
const DEVICE_TOGGLES = [
diff --git a/selfdrive/carrot/web/js/pages/tools.js b/selfdrive/carrot/web/js/pages/tools.js
index 69ae5ac32b..80e5cf319c 100644
--- a/selfdrive/carrot/web/js/pages/tools.js
+++ b/selfdrive/carrot/web/js/pages/tools.js
@@ -384,7 +384,9 @@ let toolsLanguageMenuOpen = false;
function getAvailableWebLanguages() {
const registry = window.CarrotTranslations || {};
- const order = Array.isArray(registry.order) ? registry.order : ["ko", "en", "zh"];
+ const allowed = window.CARROT_WEB_LANGUAGE_CODES || ["ko", "en", "zh"];
+ const rawOrder = Array.isArray(registry.order) ? registry.order : allowed;
+ const order = [...new Set([...rawOrder, ...allowed])].filter((lang) => allowed.includes(lang));
return order
.map((lang) => {
const pack = registry.getPack?.(lang) || registry.packs?.[lang] || {};
@@ -463,11 +465,12 @@ function renderToolsMeta() {
const languages = getAvailableWebLanguages();
const current = languages.find((item) => item.lang === LANG) || languages[0];
const langWrap = document.createElement("div");
- langWrap.className = "tools-lang-menu";
+ langWrap.className = "tools-lang-menu ui-dropdown-menu";
+ langWrap.classList.toggle("is-open", toolsLanguageMenuOpen);
const langBtn = document.createElement("button");
langBtn.type = "button";
- langBtn.className = "tools-lang-menu__button";
+ langBtn.className = "tools-lang-menu__button ui-dropdown-menu__button";
langBtn.setAttribute("aria-haspopup", "menu");
langBtn.setAttribute("aria-expanded", toolsLanguageMenuOpen ? "true" : "false");
langBtn.innerHTML = `
@@ -488,7 +491,7 @@ function renderToolsMeta() {
if (toolsLanguageMenuOpen) {
const panel = document.createElement("div");
- panel.className = "tools-lang-menu__panel";
+ panel.className = "tools-lang-menu__panel ui-dropdown-menu__panel";
panel.setAttribute("role", "menu");
const currentLabel = current?.name || LANG.toUpperCase();
panel.innerHTML = `
@@ -498,7 +501,7 @@ function renderToolsMeta() {
languages.forEach((item) => {
const option = document.createElement("button");
option.type = "button";
- option.className = "tools-lang-menu__item";
+ option.className = "tools-lang-menu__item ui-dropdown-menu__item";
option.setAttribute("role", "menuitemradio");
option.setAttribute("aria-checked", item.lang === LANG ? "true" : "false");
option.innerHTML = `
@@ -755,14 +758,6 @@ async function syncDeviceLanguageOnce() {
let targetParam = "main_en";
if (browserLang.startsWith("ko")) targetParam = "main_ko";
else if (browserLang.startsWith("zh")) targetParam = browserLang.includes("tw") || browserLang.includes("hk") ? "main_zh-CHT" : "main_zh-CHS";
- else if (browserLang.startsWith("ja")) targetParam = "main_ja";
- else if (browserLang.startsWith("de")) targetParam = "main_de";
- else if (browserLang.startsWith("fr")) targetParam = "main_fr";
- else if (browserLang.startsWith("es")) targetParam = "main_es";
- else if (browserLang.startsWith("pt")) targetParam = "main_pt-BR";
- else if (browserLang.startsWith("tr")) targetParam = "main_tr";
- else if (browserLang.startsWith("ar")) targetParam = "main_ar";
- else if (browserLang.startsWith("th")) targetParam = "main_th";
if (currentLang !== targetParam) {
await setParam("LanguageSetting", targetParam);
@@ -1343,20 +1338,8 @@ function initToolsPage() {
});
bindOnce("btnDeviceLang", async () => {
- const choices = [
- { label: "한국어", value: "main_ko" },
- { label: "English", value: "main_en" },
- { label: "中文(简体)", value: "main_zh-CHS" },
- { label: "中文(繁體)", value: "main_zh-CHT" },
- { label: "日本語", value: "main_ja" },
- { label: "Deutsch", value: "main_de" },
- { label: "Français", value: "main_fr" },
- { label: "Português", value: "main_pt-BR" },
- { label: "Español", value: "main_es" },
- { label: "Türkçe", value: "main_tr" },
- { label: "العربية", value: "main_ar" },
- { label: "ไทย", value: "main_th" },
- ];
+ const choices = (window.CarrotDeviceLanguageOptions || [])
+ .map((lang) => ({ label: lang.name, value: lang.code }));
const val = await openAppDialog({
mode: "choice",
title: getUIText("device_lang", "Device Language"),
diff --git a/selfdrive/carrot/web/js/pages/tools_settings_qr.js b/selfdrive/carrot/web/js/pages/tools_settings_qr.js
index b084e655d3..c1c99b1216 100644
--- a/selfdrive/carrot/web/js/pages/tools_settings_qr.js
+++ b/selfdrive/carrot/web/js/pages/tools_settings_qr.js
@@ -240,62 +240,19 @@ function toolsQrPreviewImageFile(file, preview, statusNode) {
}
function toolsQrSummaryHtml(summary = {}) {
- const item = (key, labelKey, fallback) => `
-
- ${Number(summary[key] || 0)}
- ${toolsQrEscape(toolsQrText(labelKey, fallback))}
-
- `;
- return `
-
- ${item("changed", "qr_restore_changed", "changed")}
- ${item("same", "qr_restore_same", "same")}
- ${item("skipped", "qr_restore_skipped", "skipped")}
- ${item("invalid", "qr_restore_invalid", "invalid")}
-
- `;
+ if (typeof renderSettingsDiffSummary === "function") return renderSettingsDiffSummary(summary);
+ return "";
}
function toolsQrDiffHtml(preview) {
- const entries = Array.isArray(preview?.entries) ? preview.entries : [];
- const changed = entries.filter((entry) => entry.apply || entry.status === "changed");
- const shown = changed.slice(0, 80);
- const hiddenCount = Math.max(0, changed.length - shown.length);
- const currentLabel = toolsQrText("qr_restore_current_value", "Current");
- const backupLabel = toolsQrText("qr_restore_backup_value", "Restore");
- const changedLabel = toolsQrText("qr_restore_changed", "Changed");
- const rows = shown.map((entry) => `
-
- `).join("");
-
- if (!changed.length) {
- return `
- ${toolsQrSummaryHtml(preview?.summary)}
-
${toolsQrEscape(toolsQrText("qr_restore_no_changes", "No changes to apply."))}
- `;
+ if (typeof renderSettingsDiffHtml === "function") {
+ return renderSettingsDiffHtml(preview, {
+ currentLabel: toolsQrText("qr_restore_current_value", "Current"),
+ nextLabel: toolsQrText("qr_restore_backup_value", "Restore"),
+ changedLabel: toolsQrText("qr_restore_changed", "Changed"),
+ });
}
-
- return `
- ${toolsQrSummaryHtml(preview?.summary)}
-
${rows}
- ${hiddenCount ? `
${toolsQrEscape(toolsQrText("qr_restore_more", "{count} more changes hidden", { count: hiddenCount }))}
` : ""}
- `;
+ return toolsQrSummaryHtml(preview?.summary);
}
async function toolsQrShowBackup() {
diff --git a/selfdrive/carrot/web/js/pages/tools_web_settings.js b/selfdrive/carrot/web/js/pages/tools_web_settings.js
index 2b16edcd8c..050365de71 100644
--- a/selfdrive/carrot/web/js/pages/tools_web_settings.js
+++ b/selfdrive/carrot/web/js/pages/tools_web_settings.js
@@ -74,7 +74,7 @@ function normalizeWebSettingValue(key, value) {
if (key === "web_language") {
if (typeof normalizeLangCode === "function") return normalizeLangCode(value);
const lang = String(value || "").trim().toLowerCase();
- return ["en", "ko", "zh", "ja", "fr"].includes(lang) ? lang : "";
+ return ["en", "ko", "zh"].includes(lang) ? lang : "";
}
return value;
}
diff --git a/selfdrive/carrot/web/js/shared/constants.js b/selfdrive/carrot/web/js/shared/constants.js
index 0d62364e59..a4f374e33d 100644
--- a/selfdrive/carrot/web/js/shared/constants.js
+++ b/selfdrive/carrot/web/js/shared/constants.js
@@ -7,8 +7,6 @@ const LANG_EMOJI = {
ko: "🇰🇷",
en: "🇺🇸",
zh: "🇨🇳",
- ja: "🇯🇵",
- fr: "🇫🇷",
};
let UNIT_CYCLE = [1, 2, 5, 10, 50, 100];
diff --git a/selfdrive/carrot/web/js/shared/i18n.js b/selfdrive/carrot/web/js/shared/i18n.js
index a4987cd6e0..9359b049bf 100644
--- a/selfdrive/carrot/web/js/shared/i18n.js
+++ b/selfdrive/carrot/web/js/shared/i18n.js
@@ -1,11 +1,20 @@
"use strict";
-// Translation registry — loaded by translations/registry.js + ko/en/zh/ja/fr.js
+// Translation registry — loaded by translations/registry.js + ko/en/zh.js
const TRANSLATION_REGISTRY = window.CarrotTranslations || { packs: {}, order: ["en", "ko", "zh"] };
const UI_STRINGS = TRANSLATION_REGISTRY.strings || {};
const ACTION_LABELS = TRANSLATION_REGISTRY.actionLabels || {};
const ERROR_MESSAGES = TRANSLATION_REGISTRY.errorMessages || {};
const DRIVE_MODES = TRANSLATION_REGISTRY.driveModes || {};
+const CARROT_WEB_LANGUAGE_CODES = Object.freeze(["en", "ko", "zh"]);
+window.CARROT_WEB_LANGUAGE_CODES = CARROT_WEB_LANGUAGE_CODES;
+
+window.CarrotDeviceLanguageOptions = Object.freeze([
+ { code: "main_en", name: "English" },
+ { code: "main_ko", name: "한국어" },
+ { code: "main_zh-CHS", name: "简体中文" },
+ { code: "main_zh-CHT", name: "繁體中文" },
+]);
let LANG = "en";
@@ -19,16 +28,12 @@ function normalizeLangCode(raw) {
main_en: "en",
"main_zh-chs": "zh",
"main_zh-cht": "zh",
- main_ja: "ja",
- main_fr: "fr",
};
if (deviceAliases[value]) return deviceAliases[value];
const withoutMainPrefix = value.replace(/^main[_-]/, "");
if (packs[withoutMainPrefix]) return withoutMainPrefix;
if (value.startsWith("ko")) return "ko";
if (value.startsWith("zh")) return "zh";
- if (value.startsWith("ja")) return "ja";
- if (value.startsWith("fr")) return "fr";
if (value.startsWith("en")) return "en";
return "";
}
@@ -287,8 +292,23 @@ function renderUIText() {
settingSearchMeta.textContent = s.setting_search_idle || "";
}
if (btnSettingSearch) {
- btnSettingSearch.setAttribute("aria-label", s.setting_search || "Search Settings");
- btnSettingSearch.title = s.setting_search || "Search Settings";
+ btnSettingSearch.setAttribute("aria-label", s.setting_action_menu || "Setting actions");
+ btnSettingSearch.title = s.setting_action_menu || "Setting actions";
+ }
+ if (settingFabSearchLabel) settingFabSearchLabel.textContent = s.setting_search || "Search Settings";
+ if (btnSettingFabSearch) {
+ btnSettingFabSearch.setAttribute("aria-label", s.setting_search || "Search Settings");
+ btnSettingFabSearch.title = s.setting_search || "Search Settings";
+ }
+ if (settingFabProfileAddLabel) settingFabProfileAddLabel.textContent = s.profile_add || "Add Profile";
+ if (btnSettingFabProfileAdd) {
+ btnSettingFabProfileAdd.setAttribute("aria-label", s.profile_add || "Add Profile");
+ btnSettingFabProfileAdd.title = s.profile_add || "Add Profile";
+ }
+ if (settingFabResetDefaultsLabel) settingFabResetDefaultsLabel.textContent = s.setting_reset_defaults || "Reset Settings";
+ if (btnSettingFabResetDefaults) {
+ btnSettingFabResetDefaults.setAttribute("aria-label", s.setting_reset_defaults || "Reset Settings");
+ btnSettingFabResetDefaults.title = s.setting_reset_defaults || "Reset Settings";
}
if (btnSettingSearchSubmit) {
btnSettingSearchSubmit.setAttribute("aria-label", s.setting_search || "Search Settings");
@@ -360,7 +380,9 @@ async function syncWebLanguageFromDeviceDefault() {
}
function toggleLang() {
- const order = (TRANSLATION_REGISTRY.order || ["en", "ko", "zh"]).filter((lang) => UI_STRINGS[lang]);
+ const allowed = CARROT_WEB_LANGUAGE_CODES;
+ const rawOrder = Array.isArray(TRANSLATION_REGISTRY.order) ? TRANSLATION_REGISTRY.order : allowed;
+ const order = [...new Set([...rawOrder, ...allowed])].filter((lang) => allowed.includes(lang) && UI_STRINGS[lang]);
const currentIndex = Math.max(0, order.indexOf(LANG));
const next = order[(currentIndex + 1) % order.length] || "en";
setWebLanguage(next);
diff --git a/selfdrive/carrot/web/js/shared/setting_diff.js b/selfdrive/carrot/web/js/shared/setting_diff.js
new file mode 100644
index 0000000000..e5ea32ec88
--- /dev/null
+++ b/selfdrive/carrot/web/js/shared/setting_diff.js
@@ -0,0 +1,92 @@
+"use strict";
+
+function settingsDiffEscape(value) {
+ return String(value ?? "")
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+}
+
+function settingsDiffText(key, fallback, vars = null) {
+ if (typeof getUIText === "function") return getUIText(key, fallback, vars);
+ let text = fallback;
+ if (vars) {
+ Object.entries(vars).forEach(([name, value]) => {
+ text = text.replaceAll(`{${name}}`, String(value));
+ });
+ }
+ return text;
+}
+
+function getSettingsDiffSelectedCount(preview) {
+ const summary = preview?.summary || {};
+ const selected = Number(summary.selected || 0);
+ if (Number.isFinite(selected) && selected > 0) return selected;
+ const entries = Array.isArray(preview?.entries) ? preview.entries : [];
+ return entries.filter((entry) => entry?.apply).length;
+}
+
+function renderSettingsDiffSummary(summary = {}) {
+ const items = [
+ ["changed", "settings_diff_changed", "changed"],
+ ["same", "settings_diff_same", "same"],
+ ["skipped", "settings_diff_skipped", "skipped"],
+ ["invalid", "settings_diff_invalid", "invalid"],
+ ];
+ return `
+
+ ${items.map(([key, labelKey, fallback]) => `
+
+ ${settingsDiffEscape(settingsDiffText(labelKey, fallback))}
+ ${settingsDiffEscape(summary?.[key] ?? 0)}
+
+ `).join("")}
+
+ `;
+}
+
+function renderSettingsDiffHtml(preview, options = {}) {
+ const entries = Array.isArray(preview?.entries) ? preview.entries : [];
+ const changed = entries.filter((entry) => entry?.apply || entry?.status === "changed");
+ const limit = Number.isFinite(options.limit) ? Math.max(1, options.limit) : 80;
+ const shown = changed.slice(0, limit);
+ const hiddenCount = Math.max(0, changed.length - shown.length);
+ const currentLabel = options.currentLabel || settingsDiffText("settings_diff_current", "Current");
+ const nextLabel = options.nextLabel || settingsDiffText("settings_diff_apply", "Apply");
+ const changedLabel = options.changedLabel || settingsDiffText("settings_diff_changed_status", "Changed");
+
+ if (!changed.length) {
+ return `
+ ${renderSettingsDiffSummary(preview?.summary)}
+
${settingsDiffEscape(settingsDiffText("settings_diff_no_changes", "No changes to apply."))}
+ `;
+ }
+
+ const rows = shown.map((entry) => `
+
+
+
${settingsDiffEscape(entry.key)}
+
${settingsDiffEscape(changedLabel)}
+
+
+
+ ${settingsDiffEscape(currentLabel)}
+ ${settingsDiffEscape(entry.current)}
+
+
>
+
+ ${settingsDiffEscape(nextLabel)}
+ ${settingsDiffEscape(entry.value)}
+
+
+
+ `).join("");
+
+ return `
+ ${renderSettingsDiffSummary(preview?.summary)}
+
${rows}
+ ${hiddenCount ? `
${settingsDiffEscape(settingsDiffText("settings_diff_more", "{count} more changes hidden", { count: hiddenCount }))}
` : ""}
+ `;
+}
diff --git a/selfdrive/carrot/web/js/shared/ui/dialog.js b/selfdrive/carrot/web/js/shared/ui/dialog.js
index cbcd4a7a46..1594639806 100644
--- a/selfdrive/carrot/web/js/shared/ui/dialog.js
+++ b/selfdrive/carrot/web/js/shared/ui/dialog.js
@@ -84,6 +84,10 @@ function resolveAppDialog(result) {
appDialogChoices.hidden = true;
appDialogChoices.innerHTML = "";
}
+ if (appDialogDefault) {
+ appDialogDefault.hidden = true;
+ appDialogDefault.onclick = null;
+ }
if (appDialogInputWrap) appDialogInputWrap.hidden = true;
if (appDialogInput) {
appDialogInput.value = "";
@@ -133,12 +137,14 @@ function openAppDialog(options = {}) {
const useHtml = Boolean(options.html);
const confirmLabel = options.confirmLabel || getUIText("ok", "OK");
const cancelLabel = options.cancelLabel || getUIText("cancel", "Cancel");
+ const defaultActionLabel = options.defaultActionLabel || "";
+ const hasDefaultAction = mode === "prompt" && Boolean(defaultActionLabel);
const choices = Array.isArray(options.choices)
? options.choices.filter((choice) => choice && (choice.label || choice.labelHtml))
: [];
const hasChoices = choices.length > 0;
const isChoice = mode === "choice" || hasChoices;
- const showCancel = mode !== "alert";
+ const showCancel = mode !== "alert" && options.showCancel !== false;
appDialogTitle.textContent = title;
if (useHtml) appDialogBody.innerHTML = String(messageHtml || message);
@@ -152,6 +158,14 @@ function openAppDialog(options = {}) {
appDialogCancel.setAttribute("aria-hidden", showCancel ? "false" : "true");
appDialogConfirm.hidden = isChoice;
appDialogConfirm.setAttribute("aria-hidden", isChoice ? "true" : "false");
+ if (appDialogDefault) {
+ appDialogDefault.hidden = !hasDefaultAction;
+ appDialogDefault.textContent = defaultActionLabel;
+ appDialogDefault.disabled = false;
+ appDialogDefault.onclick = hasDefaultAction
+ ? () => resolveAppDialog(options.defaultActionValue ?? "")
+ : null;
+ }
const copyText = options.copyText || "";
if (appDialogCopy) {
@@ -251,6 +265,9 @@ function appPrompt(message, opts = {}) {
confirmLabel: opts.confirmLabel,
cancelLabel: opts.cancelLabel,
defaultValue: opts.defaultValue,
+ defaultActionLabel: opts.defaultActionLabel,
+ defaultActionValue: opts.defaultActionValue,
+ showCancel: opts.showCancel,
placeholder: opts.placeholder,
});
}
diff --git a/selfdrive/carrot/web/js/shared/ui/navigation.js b/selfdrive/carrot/web/js/shared/ui/navigation.js
index 70b50568e0..ab4811c2bb 100644
--- a/selfdrive/carrot/web/js/shared/ui/navigation.js
+++ b/selfdrive/carrot/web/js/shared/ui/navigation.js
@@ -346,10 +346,21 @@ window.bootstrapWebStartPage = bootstrapWebStartPage;
function runPageEnter(page, prevPage, pushHistory) {
if (page === "setting") {
+ const animateOnEnter = pushHistory || prevPage !== "setting";
if (!SETTINGS && typeof loadSettings === "function") loadSettings();
else if (typeof syncSettingViewportLayout === "function" && shouldUseSettingSplitLayout("setting")) {
- syncSettingViewportLayout({ animateChrome: pushHistory || prevPage !== "setting" }).catch(() => {});
+ syncSettingViewportLayout({
+ animateChrome: animateOnEnter,
+ animateItems: animateOnEnter,
+ }).catch(() => {});
} else if (pushHistory || !CURRENT_GROUP) {
+ if (animateOnEnter) {
+ if (typeof getCurrentSettingTab === "function" && getCurrentSettingTab() === "device") {
+ if (typeof renderDeviceTab === "function") renderDeviceTab({ animateGroups: true, animateItems: true }).catch(() => {});
+ } else if (typeof renderGroups === "function") {
+ renderGroups({ animateGroups: true });
+ }
+ }
showSettingScreen("groups", false);
}
@@ -512,6 +523,7 @@ function showSettingScreen(which, pushHistory = false) {
const isGroups = (which === "groups");
const showEl = isGroups ? screenGroups : screenItems;
const hideEl = isGroups ? screenItems : screenGroups;
+ const isProfileItems = !isGroups && typeof isSettingProfileGroup === "function" && isSettingProfileGroup(CURRENT_GROUP);
const currentGroupLabel = (!isGroups && CURRENT_GROUP && typeof getSettingGroupLabel === "function")
? getSettingGroupLabel(CURRENT_GROUP)
: (CURRENT_GROUP || "");
@@ -520,6 +532,7 @@ function showSettingScreen(which, pushHistory = false) {
settingScreenHideTimer = clearPendingScreenHide(settingScreenHideTimer);
syncSettingSplitLayoutClass(splitLandscape);
+ document.getElementById("pageSetting")?.classList.toggle("setting-profile-active", isProfileItems);
if (splitLandscape) {
settingTitle.textContent = UI_STRINGS[LANG].setting || "Setting";
@@ -535,7 +548,7 @@ function showSettingScreen(which, pushHistory = false) {
if (btnBackGroups) btnBackGroups.style.display = "none";
settingTitle.textContent = isGroups ? (UI_STRINGS[LANG].setting || "Setting") : ((UI_STRINGS[LANG].setting || "Setting") + " - " + currentGroupLabel);
- if (settingSubnavWrap) settingSubnavWrap.style.display = isGroups ? "none" : "";
+ if (settingSubnavWrap) settingSubnavWrap.style.display = (isGroups || isProfileItems) ? "none" : "";
showEl.style.display = "";
requestAnimationFrame(() => {
@@ -570,7 +583,7 @@ function resetSettingPageToRoot() {
if (shouldUseSettingSplitLayout("setting")) {
if (typeof syncSettingViewportLayout === "function") {
- syncSettingViewportLayout({ animateChrome: true }).catch(() => {});
+ syncSettingViewportLayout({ animateChrome: true, animateItems: true }).catch(() => {});
}
history.replaceState({ page: "setting", screen: "items", group: CURRENT_GROUP || null }, "");
return;
diff --git a/selfdrive/carrot/web/js/translations/en.js b/selfdrive/carrot/web/js/translations/en.js
index 11cd3244b6..1ccb968164 100644
--- a/selfdrive/carrot/web/js/translations/en.js
+++ b/selfdrive/carrot/web/js/translations/en.js
@@ -248,6 +248,9 @@ window.CarrotTranslations.register("en", {
input_title: "Input",
ok: "OK",
cancel: "Cancel",
+ delete: "Delete",
+ default_value: "Default",
+ default_value_confirm: "Restore {name} to default value ({value})?",
quick_link_empty: "GithubUsername not set",
open_carrotman_confirm: "Open {name}?",
device_lang_changed: "Device language has been changed.\nPlease reboot the device to apply.",
@@ -265,11 +268,48 @@ window.CarrotTranslations.register("en", {
terminal_disconnected: "disconnected",
terminal_unavailable: "terminal unavailable",
terminal_offline: "terminal offline",
+ setting_action_menu: "Setting actions",
setting_search: "Search Settings",
+ profile_add: "Add Profile",
+ setting_profiles: "Profiles",
+ setting_profile_create_title: "Add Profile",
+ setting_profile_create_prompt: "Enter a profile name.",
+ setting_profile_name: "Profile name",
+ setting_profile_created: "Created",
+ setting_profile_info: "Info",
+ setting_profile_info_empty: "No profile metadata",
+ setting_profile_value: "Profile",
+ setting_profile_apply_title: "Apply Profile",
+ setting_profile_apply_done: "Profile applied",
+ setting_profile_apply_failed: "Failed to apply profile",
+ setting_profile_delete: "Delete Profile",
+ setting_profile_delete_confirm: "Delete this profile?\n{name}",
+ setting_profile_saved: "Profile saved",
+ setting_profile_deleted: "Profile deleted",
+ setting_profile_save_failed: "Failed to save profile",
+ setting_profile_menu: "Profile menu",
+ setting_profile_search: "Search Profile",
+ setting_profile_search_placeholder: "Search in this profile",
+ setting_reset_defaults: "Reset Settings",
+ setting_reset_defaults_confirm: "Reset all settings to defaults?",
+ setting_reset_defaults_done: "Settings reset complete",
+ setting_reset_defaults_failed: "Settings reset failed",
setting_search_placeholder: "Search name, description, group",
setting_search_empty: "No matching settings found.",
setting_search_idle: "Type to find detailed settings.",
setting_search_results: "results",
+ setting_search_all: "All settings",
+ setting_search_source_carrot: "CarrotPilot",
+ setting_search_source_profile: "Profile",
+ settings_diff_changed: "Changed",
+ settings_diff_same: "Same",
+ settings_diff_skipped: "Skipped",
+ settings_diff_invalid: "Invalid",
+ settings_diff_current: "Current",
+ settings_diff_apply: "Apply",
+ settings_diff_changed_status: "Changed",
+ settings_diff_no_changes: "No changes to apply.",
+ settings_diff_more: "{count} more changes hidden",
logs_dashcam: "Dashcam",
logs_screenrecord: "Screen Record",
display_mode: "Display mode",
diff --git a/selfdrive/carrot/web/js/translations/fr.js b/selfdrive/carrot/web/js/translations/fr.js
deleted file mode 100644
index 7e52558fc9..0000000000
--- a/selfdrive/carrot/web/js/translations/fr.js
+++ /dev/null
@@ -1,437 +0,0 @@
-"use strict";
-
-window.CarrotTranslations.register("fr", {
- name: "French",
- nativeName: "Français",
- shortName: "FR",
- strings: {
- home: "Conduite",
- setting: "Réglages",
- setting_tab_device: "Appareil",
- setting_tab_carrot: "CarrotPilot",
- tools: "Outils",
- logs: "Journaux",
- terminal: "Terminal",
- carrot: "Carrot",
- lang: "Lang",
- language: "Langue",
- current_language: "Langue actuelle",
- branch_select: "Choisir branche",
- branch_current: "Actuelle",
- server_state: "État serveur",
- working: "En cours",
- quick_link: "Link",
- car_select: "Choisir véhicule",
- makers: "Marques",
- models: "Modèles",
- groups: "Groupes",
- items: "Éléments",
- back: "Retour",
- change: "Modifier",
- git_commands: "Git Commands",
- user_system: "User / System",
- reboot: "Redémarrer",
- backup: "Sauvegarder les réglages",
- restore: "Restaurer les réglages",
- copy: "Copier",
- view: "Voir",
- device_info: "Infos appareil",
- device_tab_error: "Impossible de charger les informations de l'appareil.",
- carrot_info: "Carrot Info",
- network: "Réseau",
- toggles: "Bascules",
- software: "Logiciel",
- device_group_info: "Infos appareil",
- device_group_network: "Réseau",
- device_group_toggles: "Bascules",
- device_group_software: "Logiciel",
- device_group_developer: "Développeur",
- language_setting: "Système",
- branch: "Branche",
- commit: "Commit",
- device_type: "Appareil",
- dongle_id: "Dongle ID",
- serial: "Série",
- calibration: "Calibration",
- uncalibrated: "Non calibré",
- recent_update: "Dernière mise à jour",
- position: "Position",
- device_lang: "Langue appareil",
- reset_calib: "Réinitialiser calibration",
- reset: "Réinitialiser",
- reset_calibration: "Réinitialiser calibration",
- recalibration: "ReCalibration",
- reboot_device_desc: "Redémarrer l'appareil",
- power_off: "Éteindre",
- power_off_desc: "Éteindre l'appareil",
- power_off_confirm: "Éteindre l'appareil ?",
- reset_calibration_confirm: "Réinitialiser la calibration et redémarrer ?",
- review: "Voir",
- review_training_guide: "Revoir le guide de formation",
- review_training_desc: "Revoir les règles, fonctions et limites d'openpilot",
- review_training_confirm: "Voulez-vous revoir le guide de formation ?",
- calibration_status: "État calibration",
- calibration_status_desc: "openpilot exige que l'appareil soit monté à moins de 4° à gauche ou à droite, et à moins de 5° vers le haut ou 9° vers le bas. openpilot se calibre en continu, une réinitialisation est rarement nécessaire.",
- calibration_position_desc: "Position actuelle : pitch {pitch}°, yaw {yaw}°",
- change_language: "Changer la langue",
- pair_device: "Pair Device",
- pair_device_desc: "Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.",
- pair: "PAIR",
- driver_camera: "Driver Camera",
- driver_camera_desc: "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)",
- preview: "PREVIEW",
- regulatory: "Regulatory",
- view_upper: "VIEW",
- show_upper: "SHOW",
- advanced: "Advanced",
- enable_tethering: "Enable Tethering",
- tethering_password: "Tethering Password",
- ip_address: "IP Address",
- enable_roaming: "Enable Roaming",
- apn_setting: "APN Setting",
- automatic: "automatic",
- edit_upper: "EDIT",
- cellular_metered: "Cellular Metered",
- cellular_metered_desc: "Prevent large data uploads when on a metered connection",
- hidden_network: "Hidden Network",
- connect_upper: "CONNECT",
- updates_offroad_only: "Updates are only downloaded while the car is off.",
- download: "Télécharger",
- check_upper: "CHECK",
- install_update: "Install Update",
- install_upper: "INSTALL",
- select_upper: "SELECT",
- uninstall_openpilot: "Uninstall openpilot",
- uninstall_upper: "UNINSTALL",
- driving_personality: "Driving Personality",
- current_version: "Current Version",
- target_branch: "Target Branch",
- update_state: "Update State",
- update_available: "Update Available",
- language_note: "Requires reboot",
- restore_defaults: "Restore Defaults",
- restore_defaults_desc: "Restore stock settings",
- restore_defaults_confirm: "Restore defaults and reboot?",
- yes: "Yes",
- no: "No",
- enable_openpilot: "Enable openpilot",
- experimental_mode: "Experimental Mode",
- experimental_mode_confirm: "Le mode expérimental active des fonctions alpha qui ne sont pas encore prêtes pour le mode normal. Activer le mode expérimental ?",
- disengage_on_accelerator: "Disengage on Accelerator",
- enable_ldw: "Enable Lane Departure Warnings",
- always_on_dm: "Always-on DM",
- record_front: "Record and Upload Driver Camera",
- record_audio: "Record and Upload Microphone Audio",
- record_front_lock: "Record audio",
- is_metric: "Use Metric System",
- enable_adb: "Activer ADB",
- enable_ssh: "Activer SSH",
- ssh_keys: "Clés SSH",
- ssh_keys_desc: "Attention : cela donne un accès SSH à toutes les clés publiques de vos réglages GitHub. N'entrez jamais un autre nom d'utilisateur GitHub que le vôtre.",
- ssh_github_username_prompt: "Entrez votre nom d'utilisateur GitHub",
- ssh_keys_added: "Clés SSH ajoutées",
- ssh_keys_removed: "Clés SSH supprimées",
- add_upper: "ADD",
- remove_upper: "REMOVE",
- not_configured: "Non configuré",
- web_settings: "Réglages web",
- web_settings_general: "Général",
- web_settings_display: "Affichage",
- web_settings_empty: "Aucun réglage web général pour le moment.",
- web_auto_update: "Mise à jour auto",
- web_auto_update_desc: "Exécute automatiquement git pull lorsque des mises à jour sont disponibles. Aucun redémarrage ne sera lancé.",
- web_auto_update_running: "Mise à jour auto : git pull en cours.",
- web_auto_update_done: "Mise à jour auto terminée. Aucun redémarrage demandé.",
- web_auto_update_failed: "Échec de la mise à jour auto",
- web_start_page: "Menu de départ",
- web_start_page_desc: "Choisir le menu ouvert en premier au chargement de Carrot Web.",
- web_start_page_last: "Dernier onglet",
- tools_notifications: "Notifications",
- tools_notifications_other: "Autre",
- tools_notifications_empty: "Aucune notification",
- tools_notifications_clear: "Effacer",
- tools_notifications_no_output: "(aucune sortie)",
- tools_notification_detail: "Journal détaillé",
- enable: "Activer",
- adb_enable_confirm: "ADB permet de se connecter à votre appareil par USB ou par le réseau. Activer ADB ?",
- alpha_longitudinal_confirm: "AVERTISSEMENT : le contrôle longitudinal openpilot est en alpha pour cette voiture et désactivera le freinage d'urgence automatique (AEB).\n\nActivez ceci pour passer de l'ACC intégré de la voiture au contrôle longitudinal openpilot. Il est recommandé d'activer aussi Experimental Mode.",
- joystick_debug_mode: "Mode debug joystick",
- longitudinal_maneuver_mode: "Mode manoeuvre longitudinale",
- alpha_longitudinal_control: "Contrôle longitudinal openpilot (Alpha)",
- relaxed: "Relaxed",
- standard: "Standard",
- aggressive: "Aggressive",
- more_relaxed: "MoreRelaxed",
- driving_personality_desc: "Aggressive, Standard, Relaxed, MoreRelaxed",
- scanning_networks: "Scanning for networks...",
- wifi_viewer_only: "Viewer only",
- connected: "Connecté",
- not_connected: "Not connected",
- secured: "Secured",
- open_network: "Open",
- next: "Next",
- close: "Fermer",
- action_triggered: "Action triggered",
- device_only_control: "This can only be controlled on the device.",
- regulatory_load_failed: "Failed to load regulatory information.",
- capture_tmux: "capture tmux",
- send_tmux: "send tmux",
- install_required: "install flask",
- delete_all_videos: "delete all videos",
- delete_all_logs: "delete all logs",
- rebuild_all: "Rebuild All",
- change_repository: "change repository",
- change_branch: "change branch",
- add_remote: "add remote",
- reset_repo: "reset repo",
- apply: "Appliquer",
- confirm_car: "Choisir ce véhicule ?",
- confirm_reboot: "Redémarrer maintenant ?",
- confirm_reboot_after_install: "Installation terminée.\nUn redémarrage est nécessaire pour appliquer les changements.\nRedémarrer maintenant ?",
- reboot_later: "Sélectionné. Redémarrez plus tard pour appliquer.",
- rebooting: "Redémarrage...",
- git_sync_confirm: "Les branches vont être synchronisées.\nLes branches locales peuvent être nettoyées. Continuer ?",
- git_reset_confirm: "Les changements de code vont être annulés.\nVos modifications peuvent être perdues. Continuer ?",
- git_reset_mode_prompt: "Choisir le mode de reset\n\n• hard : supprimer tous les changements\n• soft : annuler le commit seulement\n• mixed : retirer de l'index seulement",
- git_reset_target_prompt: "Entrer la cible de reset\nex. HEAD (actuel), origin/master (distant)",
- delete_videos_confirm: "Supprimer TOUTES les vidéos de conduite ?\nCette action est irréversible. Continuer ?",
- delete_logs_confirm: "Supprimer TOUS les fichiers journaux ?\nCette action est irréversible. Continuer ?",
- rebuild_confirm: "Lancer une reconstruction complète ?\nLes fichiers de build seront supprimés et l'appareil redémarrera.\nCela peut prendre plusieurs minutes. Continuer ?",
- select_backup_file: "Choisissez d'abord un fichier JSON de sauvegarde.",
- restore_confirm: "Restaurer les réglages depuis un fichier ?\n\nCela écrasera de nombreuses valeurs Params.",
- restore_done_reboot: "Restauration terminée.\nRedémarrer maintenant ?",
- checkout_confirm: "Basculer vers cette branche ?",
- branch_changed: "Branche changée.",
- quick_link_hint: "Long press to save link",
- failed_set_car: "Failed to set car: ",
- open_car_select: "Ouvrir le choix véhicule",
- open_car_select_named: "Ouvrir le choix véhicule pour {name}",
- missing_car_select: "Aucun véhicule sélectionné.\nVeuillez d'abord choisir un véhicule dans les réglages.",
- reboot_failed: "Échec du redémarrage : ",
- set_failed: "Échec du réglage : ",
- setting_value_edit: "Modifier la valeur",
- setting_value_title: "Modifier la valeur",
- setting_value_prompt: "Entrer la valeur pour {name}\nPlage : {min} - {max}",
- setting_value_invalid: "Entrez un nombre valide.",
- setting_favorites: "Favoris",
- setting_favorites_empty_title: "Aucun favori",
- setting_favorites_empty_desc: "Appuyez longuement sur un réglage pour l'ajouter. Appuyez longuement à nouveau pour le retirer.",
- setting_favorite_added: "Ajouté aux favoris",
- setting_favorite_removed: "Retiré des favoris",
- setting_favorites_save_failed: "Échec de l'enregistrement des favoris",
- branch_dom_missing: "Branch DOM elements missing.",
- fullscreen_not_supported: "Fullscreen not supported on this browser.",
- record: "Enregistrer",
- record_on: "Enregistrement",
- record_off: "En attente",
- ready: "Prêt",
- loading: "Chargement...",
- tool_queued: "En file d'attente...",
- tool_queued_detail: "En attente : {command}",
- just_now: "à l'instant",
- minutes_ago: "il y a {count} min",
- hours_ago: "il y a {count} h",
- days_ago: "il y a {count} j",
- open: "Ouvrir",
- save: "Enregistrer",
- copied: "Copié",
- sent: "Envoyé",
- failed: "Échec",
- not_set: "Non défini",
- connecting: "Connexion...",
- reconnecting: "Reconnexion...",
- error: "Erreur",
- notice: "Info",
- confirm_title: "Confirmer",
- input_title: "Entrée",
- ok: "OK",
- cancel: "Annuler",
- quick_link_empty: "GithubUsername non défini",
- open_carrotman_confirm: "Ouvrir {name} ?",
- device_lang_changed: "La langue de l'appareil a été modifiée.\nRedémarrez l'appareil pour appliquer.",
- section_settings_backup: "Settings",
- section_sys_cmd: "Commande systeme",
- section_output: "Sortie",
- sys_cmd_help: "Autorise: pull, status, branch, log, git ..., df, free, uptime",
- terminal_session: "tmux carrot-web",
- terminal_placeholder: "git status",
- terminal_send: "Envoyer",
- terminal_reconnect: "Reconnecter",
- terminal_ctrl_c: "Ctrl+C",
- terminal_clear: "Clear",
- terminal_ready: "tmux prêt",
- terminal_disconnected: "déconnecté",
- terminal_unavailable: "terminal indisponible",
- terminal_offline: "terminal hors ligne",
- setting_search: "Rechercher réglages",
- setting_search_placeholder: "Nom, description, groupe",
- setting_search_empty: "Aucun réglage trouvé.",
- setting_search_idle: "Tapez pour trouver un réglage.",
- setting_search_results: "Résultats",
- logs_dashcam: "Dashcam",
- logs_screenrecord: "Enregistrement écran",
- display_mode: "Mode d'affichage",
- display_fit: "Ajusté",
- display_normal: "Taille normale",
- display_crop: "Recadré",
- e2e_driving: "Conduite E2E",
- start_vision: "Carrot Vision",
- start_vision_hint: "Touchez le bouton central pour activer Carrot Vision.",
- waiting_road_stream: "En attente du flux caméra route...",
- waiting_server: "En attente du serveur...",
- connected_waiting_track: "Connecté, attente de la piste vidéo...",
- no_track_retry: "Aucune piste, nouvelle tentative...",
- video_track_lost_reconnecting: "Piste vidéo perdue, reconnexion...",
- video_stalled_reconnecting: "Vidéo bloquée, reconnexion...",
- no_initial_frame_reconnecting: "Aucune première image, reconnexion...",
- vision_unavailable_title: "Carrot Vision indisponible",
- vision_unavailable_hint: "Disponible lorsque DisableDM vaut 2.",
- vision_step_unavailable: "Activez DisableDM 2 pour utiliser Carrot Vision.",
- vision_step_inactive: "Prêt à démarrer.",
- vision_step_starting: "Préparation des flux caméra et overlay.",
- vision_step_rtc_connecting: "Ouverture du flux WebRTC de la caméra route.",
- vision_step_track_waiting: "Flux connecté. En attente de la piste vidéo.",
- vision_step_first_frame: "Piste vidéo reçue. En attente de la première image.",
- vision_step_ready: "Caméra et overlay en direct.",
- vision_step_recovering: "Actualisation de la connexion du flux.",
- vision_step_failed: "Échec de la vérification. Nouvelle tentative si disponible.",
- vision_step_waiting_runtime: "En attente de la connexion runtime du véhicule.",
- vision_step_waiting_car: "En attente des services caméra du véhicule.",
- dashcam_empty: "Aucun trajet enregistré.",
- dashcam_empty_title: "No dashcam records",
- dashcam_empty_desc: "Driving routes and video segments will appear here after recording.",
- selected_count: "{count} sélectionné(s)",
- select_all: "Tout sélectionner",
- deselect_all: "Tout désélectionner",
- upload_selected: "Envoyer sélection",
- segment_count: "{count} segments",
- segment_menu: "Menu segment",
- show_segments: "Voir segments",
- collapse: "Réduire",
- log_upload: "Envoi logs",
- log_upload_confirm: "Envoyer {count} logs au serveur Carrot ?",
- upload_data_warning: "This upload may use mobile data depending on your network connection.",
- upload_file_count: "{count} files",
- upload_files_unknown: "files unknown",
- upload_size_unknown: "size unknown",
- log_uploading: "Envoi des logs",
- log_upload_result: "Résultat envoi logs",
- upload_count: "Envoi {uploaded}/{total}",
- upload_complete_count: "Envoi terminé {uploaded}/{total}",
- upload_canceled: "Envoi annulé.",
- upload_canceling: "Annulation...",
- upload_already_running: "Un envoi est déjà en cours.",
- dashcam_load_failed: "Échec du chargement dashcam",
- screenrecord_empty: "Aucun enregistrement écran.",
- screenrecord_empty_title: "No screen recordings",
- screenrecord_empty_desc: "Screen recording files will appear here after recording.",
- screenrecord_load_failed: "Échec du chargement des enregistrements écran",
- toggle_log_panel: "Développer ou réduire le panneau de journal",
- git_reset_head_prompt: "Choisir le mode de reset basé sur HEAD.",
- disable_dm_inactive: "Disponible lorsque DisableDM vaut 2.",
- disable_dm_check_failed: "Impossible de vérifier l'état de DisableDM.",
- waiting_model: "attente de modelV2...",
- no_selected_segments: "Aucun segment sélectionné.",
- play: "Lire",
- video_controls: "Contrôles vidéo",
- rewind_5: "Reculer 5 s",
- forward_5: "Avancer 5 s",
- pause: "Pause",
- ended: "Fin",
- muted: "Muet",
- fullscreen: "Plein écran",
- fullscreen_exit: "Quitter plein écran",
- pip_exit: "Quitter PiP",
- git_remote_title: "Changer le dépôt",
- git_remote_prompt: "Actuel : {url}\n\nEntrez la nouvelle URL du dépôt GitHub.\n(Cela remplacera la connexion actuelle)",
- git_remote_fetching: "Récupération des données du dépôt.\nCela peut prendre quelques minutes pour un nouveau dépôt.\nVeuillez patienter...",
- git_remote_success: "Dépôt changé avec succès.\nCliquez sur [change branch] pour choisir une branche.",
- git_add_remote_title: "Ajouter/mettre à jour un remote",
- git_add_remote_name_prompt: "Entrez le nom du remote (ex. remote)",
- git_add_remote_url_prompt: "Entrez l'URL pour '{name}'",
- git_add_remote_done: "Remote '{name}' ajouté/mis à jour",
- git_log_checkout_prompt: "Choisir le commit à checkout",
- git_log_checkout_confirm: "Checkout ce commit ?",
- git_log_checkout_done: "Checkout terminé",
- git_reset_repo_title: "Réinitialiser le dépôt",
- git_reset_repo_confirm: "Attention : cela supprimera origin et rajoutera 'ajouatom/openpilot'.\nTous les changements locaux seront perdus. Continuer ?",
- git_reset_repo_no_branches: "Aucune branche trouvée",
- git_reset_repo_branch_message: "Choisir la branche cible de reset",
- git_reset_repo_done: "Reset vers '{branch}' terminé",
- reset_calib_title: "ReCalibration",
- reset_calib_confirm: "Voulez-vous réinitialiser la calibration ?\nL'appareil redémarrera automatiquement.",
- device_lang_select_prompt: "Choisir la langue de l'interface de l'appareil",
- setting_changed_reboot: "Réglage modifié. Redémarrer maintenant ?",
- settings_not_loaded: "Réglages non chargés",
- copy_settings_done: "{count} Params copiés",
- settings_title: "Réglages ({count} Params)",
- qr_backup: "Sauvegarde QR",
- qr_restore: "Restauration QR",
- qr_backup_title: "Sauvegarde QR",
- qr_restore_title: "Restauration QR",
- qr_backup_count: "{count} Params",
- qr_backup_size: "{chars} caractères",
- qr_configuring: "Veuillez patienter...",
- qr_config_done: "La fonction QR est prête.",
- qr_config_failed: "La fonction QR n'a pas pu être configurée.",
- qr_restore_upload: "Image",
- qr_restore_camera: "Caméra",
- qr_restore_camera_disabled: "Caméra indisponible",
- qr_restore_stop_camera: "Arrêter caméra",
- qr_restore_paste_placeholder: "Coller le texte de sauvegarde QR",
- qr_restore_check: "Vérifier",
- qr_restore_hint: "Scannez avec la caméra ou choisissez une image QR avant de restaurer.",
- qr_restore_scan_hint: "Pointez la caméra vers le QR code.",
- qr_restore_scan_detected: "Placez le QR code dans le cadre de guidage.",
- qr_restore_scan_aligned: "QR code aligné. Restez immobile...",
- qr_restore_scan_locked: "QR code capturé.",
- qr_restore_decode_failed: "QR code introuvable.",
- qr_restore_previewing: "Vérification de la sauvegarde...",
- qr_restore_ready: "{count} modifications prêtes",
- qr_restore_no_changes: "Aucune modification à appliquer.",
- qr_restore_apply: "Appliquer",
- qr_restore_changed: "Modifié",
- qr_restore_current_value: "Actuel",
- qr_restore_backup_value: "Restaurer",
- qr_restore_same: "Identique",
- qr_restore_skipped: "Ignoré",
- qr_restore_invalid: "Invalide",
- qr_restore_more: "{count} modifications supplémentaires masquées",
- qr_restore_applied: "{count} Params restaurés",
- qr_restore_https_required: "Ouvrez cette page en HTTPS pour utiliser la caméra.",
- qr_restore_camera_unsupported: "Le flux caméra n'est pas pris en charge par ce navigateur.",
- qr_restore_camera_failed: "Impossible d'ouvrir la caméra.",
- qr_restore_preview_failed: "Impossible de lire la sauvegarde.",
- empty_value: "(vide)",
- },
- actionLabels: {
- git_pull: { running: "Recherche des mises à jour...", done: "Mise à jour terminée", failed: "Échec de la mise à jour" },
- git_sync: { running: "Synchronisation des branches...", done: "Synchronisation terminée", failed: "Échec de synchronisation" },
- git_reset: { running: "Réinitialisation...", done: "Réinitialisation terminée", failed: "Échec de réinitialisation" },
- git_checkout: { running: "Changement de branche...", done: "Branche changée", failed: "Échec du changement de branche" },
- git_branch_list: { running: "Chargement des branches...", done: "Branches chargées", failed: "Échec du chargement" },
- reboot: { running: "Demande de redémarrage...", done: "Redémarrage lancé", failed: "Échec du redémarrage" },
- send_tmux_log: { running: "Téléchargement du journal...", done: "Téléchargement terminé", failed: "Échec du téléchargement" },
- server_tmux_log: { running: "Envoi du journal serveur...", done: "Envoyé", failed: "Échec de l'envoi" },
- backup_settings: { running: "Sauvegarde des réglages...", done: "Sauvegarde terminée", failed: "Échec de sauvegarde" },
- delete_all_videos: { running: "Suppression des vidéos...", done: "Supprimé", failed: "Échec de suppression" },
- delete_all_logs: { running: "Suppression des journaux...", done: "Supprimé", failed: "Échec de suppression" },
- rebuild_all: { running: "Reconstruction complète...", done: "Reconstruction + redémarrage lancés", failed: "Échec de reconstruction" },
- shell_cmd: { running: "Commande en cours...", done: "Terminé", failed: "Échec de commande" },
- install_required: { running: "Installation des paquets...", done: "Installé", failed: "Échec d'installation" },
- git_remote_add: { running: "Ajout/mise à jour du remote...", done: "Remote ajouté/mis à jour", failed: "Échec ajout/mise à jour remote" },
- git_log: { running: "Chargement des commits...", done: "Chargé", failed: "Échec du chargement" },
- git_reset_repo_fetch: { running: "Récupération des infos dépôt...", done: "Récupération terminée", failed: "Échec de récupération" },
- git_reset_repo_checkout: { running: "Réinitialisation du dépôt...", done: "Réinitialisation terminée", failed: "Échec de réinitialisation" },
- reset_calib: { running: "Réinitialisation de la calibration...", done: "Réinitialisation terminée", failed: "Échec de réinitialisation" },
- },
- errorMessages: {
- GIT_CMD_NOT_ALLOWED: (d) => `This git command is not allowed: ${d}`,
- CMD_NOT_ALLOWED: (d) => `This command is not allowed: ${d}`,
- INVALID_RESET_MODE: () => "Invalid reset mode",
- MISSING_BRANCH: () => "Please select a branch",
- CMD_TIMEOUT: () => "Command timed out",
- TMUX_CAPTURE_FAIL: () => "Failed to capture log",
- },
- driveModes: { normal: "Normal", eco: "Eco", safe: "Safe", sport: "Sport" },
-});
diff --git a/selfdrive/carrot/web/js/translations/ja.js b/selfdrive/carrot/web/js/translations/ja.js
deleted file mode 100644
index 262eccb74e..0000000000
--- a/selfdrive/carrot/web/js/translations/ja.js
+++ /dev/null
@@ -1,437 +0,0 @@
-"use strict";
-
-window.CarrotTranslations.register("ja", {
- name: "Japanese",
- nativeName: "日本語",
- shortName: "JA",
- strings: {
- home: "ドライブ",
- setting: "設定",
- setting_tab_device: "デバイス",
- setting_tab_carrot: "CarrotPilot",
- tools: "ツール",
- logs: "ログ",
- terminal: "ターミナル",
- carrot: "Carrot",
- lang: "Lang",
- language: "Language",
- current_language: "現在の言語",
- branch_select: "ブランチ選択",
- branch_current: "現在",
- server_state: "サーバー状態",
- working: "処理中",
- quick_link: "Link",
- car_select: "車両選択",
- makers: "メーカー",
- models: "モデル",
- groups: "グループ",
- items: "項目",
- back: "戻る",
- change: "変更",
- git_commands: "Git Commands",
- user_system: "User / System",
- reboot: "再起動",
- backup: "設定バックアップ",
- restore: "設定復元",
- copy: "Copy",
- view: "View",
- device_info: "デバイス情報",
- device_tab_error: "デバイス情報の読み込みに失敗しました。",
- carrot_info: "Carrot Info",
- network: "ネットワーク",
- toggles: "トグル",
- software: "ソフトウェア",
- device_group_info: "デバイス情報",
- device_group_network: "ネットワーク",
- device_group_toggles: "トグル",
- device_group_software: "ソフトウェア",
- device_group_developer: "開発者",
- language_setting: "システム",
- branch: "ブランチ",
- commit: "コミット",
- device_type: "デバイス",
- dongle_id: "Dongle ID",
- serial: "シリアル",
- calibration: "キャリブレーション",
- uncalibrated: "未キャリブレーション",
- recent_update: "最近の更新",
- position: "位置",
- device_lang: "デバイス言語",
- reset_calib: "キャリブレーション初期化",
- reset: "リセット",
- reset_calibration: "キャリブレーションをリセット",
- recalibration: "再キャリブレーション",
- reboot_device_desc: "デバイスを再起動します",
- power_off: "電源オフ",
- power_off_desc: "デバイスの電源を切ります",
- power_off_confirm: "デバイスの電源を切りますか?",
- reset_calibration_confirm: "キャリブレーションをリセットして再起動しますか?",
- review: "確認",
- review_training_guide: "トレーニングガイドを確認",
- review_training_desc: "openpilot のルール、機能、制限を確認します",
- review_training_confirm: "トレーニングガイドを確認しますか?",
- calibration_status: "キャリブレーション状態",
- calibration_status_desc: "openpilot では、デバイスを左右 4° 以内、上 5° または下 9° 以内に取り付ける必要があります。openpilot は継続的にキャリブレーションするため、リセットが必要になることはまれです。",
- calibration_position_desc: "現在位置: pitch {pitch}°, yaw {yaw}°",
- change_language: "言語変更",
- pair_device: "Pair Device",
- pair_device_desc: "Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.",
- pair: "PAIR",
- driver_camera: "Driver Camera",
- driver_camera_desc: "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)",
- preview: "PREVIEW",
- regulatory: "Regulatory",
- view_upper: "VIEW",
- show_upper: "SHOW",
- advanced: "Advanced",
- enable_tethering: "Enable Tethering",
- tethering_password: "Tethering Password",
- ip_address: "IP Address",
- enable_roaming: "Enable Roaming",
- apn_setting: "APN Setting",
- automatic: "automatic",
- edit_upper: "EDIT",
- cellular_metered: "Cellular Metered",
- cellular_metered_desc: "Prevent large data uploads when on a metered connection",
- hidden_network: "Hidden Network",
- connect_upper: "CONNECT",
- updates_offroad_only: "Updates are only downloaded while the car is off.",
- download: "ダウンロード",
- check_upper: "CHECK",
- install_update: "Install Update",
- install_upper: "INSTALL",
- select_upper: "SELECT",
- uninstall_openpilot: "Uninstall openpilot",
- uninstall_upper: "UNINSTALL",
- driving_personality: "Driving Personality",
- current_version: "Current Version",
- target_branch: "Target Branch",
- update_state: "Update State",
- update_available: "Update Available",
- language_note: "Requires reboot",
- restore_defaults: "Restore Defaults",
- restore_defaults_desc: "Restore stock settings",
- restore_defaults_confirm: "Restore defaults and reboot?",
- yes: "Yes",
- no: "No",
- enable_openpilot: "Enable openpilot",
- experimental_mode: "Experimental Mode",
- experimental_mode_confirm: "実験モードは、通常モード向けに準備が完了していないアルファ機能を有効にします。実験モードを有効にしますか?",
- disengage_on_accelerator: "Disengage on Accelerator",
- enable_ldw: "Enable Lane Departure Warnings",
- always_on_dm: "Always-on DM",
- record_front: "Record and Upload Driver Camera",
- record_audio: "Record and Upload Microphone Audio",
- record_front_lock: "Record audio",
- is_metric: "Use Metric System",
- enable_adb: "ADB を有効化",
- enable_ssh: "SSH を有効化",
- ssh_keys: "SSH キー",
- ssh_keys_desc: "警告: GitHub 設定内のすべての公開鍵に SSH アクセスを許可します。必ず自分の GitHub ユーザー名だけを入力してください。",
- ssh_github_username_prompt: "GitHub ユーザー名を入力してください",
- ssh_keys_added: "SSH キーを追加しました",
- ssh_keys_removed: "SSH キーを削除しました",
- add_upper: "ADD",
- remove_upper: "REMOVE",
- not_configured: "未設定",
- web_settings: "Web 設定",
- web_settings_general: "一般",
- web_settings_display: "表示",
- web_settings_empty: "一般の Web 設定はまだありません。",
- web_auto_update: "自動更新",
- web_auto_update_desc: "更新がある場合は自動的に git pull を実行します。再起動はしません。",
- web_auto_update_running: "自動更新: git pull を実行中です。",
- web_auto_update_done: "自動更新が完了しました。再起動は要求していません。",
- web_auto_update_failed: "自動更新に失敗しました",
- web_start_page: "開始メニュー",
- web_start_page_desc: "Carrot Web の読み込み時に最初に開くメニューを選択します。",
- web_start_page_last: "最後のタブ",
- tools_notifications: "通知",
- tools_notifications_other: "その他",
- tools_notifications_empty: "通知はありません",
- tools_notifications_clear: "消去",
- tools_notifications_no_output: "(出力なし)",
- tools_notification_detail: "詳細ログ",
- enable: "有効化",
- adb_enable_confirm: "ADB は USB またはネットワーク経由でデバイスへ接続できるようにします。ADB を有効にしますか?",
- alpha_longitudinal_confirm: "警告: この車両の openpilot 縦方向制御は alpha 段階であり、自動緊急ブレーキ(AEB)を無効化します。\n\n車両内蔵 ACC ではなく openpilot 縦方向制御に切り替えます。Experimental Mode も有効にすることを推奨します。",
- joystick_debug_mode: "ジョイスティックデバッグモード",
- longitudinal_maneuver_mode: "縦方向マニューバーモード",
- alpha_longitudinal_control: "openpilot 縦方向制御(Alpha)",
- relaxed: "Relaxed",
- standard: "Standard",
- aggressive: "Aggressive",
- more_relaxed: "MoreRelaxed",
- driving_personality_desc: "Aggressive, Standard, Relaxed, MoreRelaxed",
- scanning_networks: "Scanning for networks...",
- wifi_viewer_only: "Viewer only",
- connected: "接続済み",
- not_connected: "Not connected",
- secured: "Secured",
- open_network: "Open",
- next: "Next",
- close: "閉じる",
- action_triggered: "Action triggered",
- device_only_control: "This can only be controlled on the device.",
- regulatory_load_failed: "Failed to load regulatory information.",
- capture_tmux: "capture tmux",
- send_tmux: "send tmux",
- install_required: "install flask",
- delete_all_videos: "delete all videos",
- delete_all_logs: "delete all logs",
- rebuild_all: "Rebuild All",
- change_repository: "change repository",
- change_branch: "change branch",
- add_remote: "add remote",
- reset_repo: "reset repo",
- apply: "適用",
- confirm_car: "この車両を選択しますか?",
- confirm_reboot: "今すぐ再起動しますか?",
- confirm_reboot_after_install: "インストールが完了しました。\n変更を適用するには再起動が必要です。\n今すぐ再起動しますか?",
- reboot_later: "選択しました。適用には後で再起動してください。",
- rebooting: "再起動中...",
- git_sync_confirm: "ブランチを同期します。\nローカルブランチが整理される場合があります。続行しますか?",
- git_reset_confirm: "コード変更を元に戻します。\n変更内容が失われる場合があります。続行しますか?",
- git_reset_mode_prompt: "Select reset mode\n\n• hard: discard all changes\n• soft: undo commit only\n• mixed: unstage only",
- git_reset_target_prompt: "Enter reset target\ne.g. HEAD (current), origin/master (remote)",
- delete_videos_confirm: "すべての走行動画を削除しますか?\nこの操作は元に戻せません。続行しますか?",
- delete_logs_confirm: "すべてのログファイルを削除しますか?\nこの操作は元に戻せません。続行しますか?",
- rebuild_confirm: "フルリビルドを実行しますか?\nビルドファイルが削除され、デバイスが再起動します。\n数分かかる場合があります。続行しますか?",
- select_backup_file: "先にバックアップ JSON ファイルを選択してください。",
- restore_confirm: "ファイルから設定を復元しますか?\n\n多くの Params 値が上書きされます。",
- restore_done_reboot: "復元が完了しました。\n今すぐ再起動しますか?",
- checkout_confirm: "このブランチに切り替えますか?",
- branch_changed: "ブランチを変更しました。",
- quick_link_hint: "Long press to save link",
- failed_set_car: "Failed to set car: ",
- open_car_select: "車両選択を開く",
- open_car_select_named: "{name} の車両選択を開く",
- missing_car_select: "車両が選択されていません。\n設定で先に車両を選択してください。",
- reboot_failed: "再起動に失敗しました: ",
- set_failed: "設定に失敗しました: ",
- setting_value_edit: "値を編集",
- setting_value_title: "値を編集",
- setting_value_prompt: "{name} の値を入力してください\n範囲: {min} - {max}",
- setting_value_invalid: "有効な数値を入力してください。",
- setting_favorites: "お気に入り",
- setting_favorites_empty_title: "お気に入りなし",
- setting_favorites_empty_desc: "設定項目を長押しすると追加され、もう一度長押しすると削除されます。",
- setting_favorite_added: "お気に入りに追加しました",
- setting_favorite_removed: "お気に入りから削除しました",
- setting_favorites_save_failed: "お気に入りの保存に失敗しました",
- branch_dom_missing: "Branch DOM elements missing.",
- fullscreen_not_supported: "Fullscreen not supported on this browser.",
- record: "録画",
- record_on: "録画中",
- record_off: "待機中",
- ready: "準備完了",
- loading: "読み込み中...",
- tool_queued: "キューに追加しました...",
- tool_queued_detail: "待機中: {command}",
- just_now: "たった今",
- minutes_ago: "{count}分前",
- hours_ago: "{count}時間前",
- days_ago: "{count}日前",
- open: "開く",
- save: "保存",
- copied: "コピーしました",
- sent: "送信完了",
- failed: "失敗",
- not_set: "未設定",
- connecting: "接続中...",
- reconnecting: "再接続中...",
- error: "エラー",
- notice: "通知",
- confirm_title: "確認",
- input_title: "入力",
- ok: "OK",
- cancel: "キャンセル",
- quick_link_empty: "GithubUsername が未設定です",
- open_carrotman_confirm: "{name} を開きますか?",
- device_lang_changed: "デバイス言語を変更しました。\n適用するにはデバイスを再起動してください。",
- section_settings_backup: "Settings",
- section_sys_cmd: "システムコマンド",
- section_output: "出力",
- sys_cmd_help: "許可: pull, status, branch, log, git ..., df, free, uptime",
- terminal_session: "tmux carrot-web",
- terminal_placeholder: "git status",
- terminal_send: "送信",
- terminal_reconnect: "再接続",
- terminal_ctrl_c: "Ctrl+C",
- terminal_clear: "Clear",
- terminal_ready: "tmux ready",
- terminal_disconnected: "disconnected",
- terminal_unavailable: "terminal unavailable",
- terminal_offline: "terminal offline",
- setting_search: "設定検索",
- setting_search_placeholder: "名前、説明、グループを検索",
- setting_search_empty: "一致する設定がありません。",
- setting_search_idle: "入力すると詳細設定を検索できます。",
- setting_search_results: "検索結果",
- logs_dashcam: "ドライブ録画",
- logs_screenrecord: "画面録画",
- display_mode: "表示モード",
- display_fit: "縮小",
- display_normal: "標準サイズ",
- display_crop: "クロップ",
- e2e_driving: "E2E走行中",
- start_vision: "Carrot Vision",
- start_vision_hint: "中央のボタンを押すと Carrot Vision が有効になります。",
- waiting_road_stream: "ロードカメラのストリームを待機中...",
- waiting_server: "サーバー待機中...",
- connected_waiting_track: "接続済み、ビデオトラック待機中...",
- no_track_retry: "トラックなし、再試行中...",
- video_track_lost_reconnecting: "ビデオトラックが失われました。再接続中...",
- video_stalled_reconnecting: "映像が停止しました。再接続中...",
- no_initial_frame_reconnecting: "初期フレームなし、再接続中...",
- vision_unavailable_title: "Carrot Vision を使用できません",
- vision_unavailable_hint: "DisableDM が 2 のとき使用できます。",
- vision_step_unavailable: "DisableDM を 2 にすると Carrot Vision を使用できます。",
- vision_step_inactive: "開始待機中です。",
- vision_step_starting: "カメラとオーバーレイのストリームを準備中です。",
- vision_step_rtc_connecting: "ロードカメラの WebRTC ストリームを開いています。",
- vision_step_track_waiting: "ストリーム接続済み、ビデオトラック待機中です。",
- vision_step_first_frame: "ビデオトラック受信済み、最初のフレームを待機中です。",
- vision_step_ready: "カメラとオーバーレイはライブです。",
- vision_step_recovering: "ストリーム接続を更新中です。",
- vision_step_failed: "接続確認に失敗しました。利用可能になり次第再試行します。",
- vision_step_waiting_runtime: "車両ランタイム接続を待機中です。",
- vision_step_waiting_car: "車両カメラサービスを待機中です。",
- dashcam_empty: "走行記録がありません。",
- dashcam_empty_title: "No dashcam records",
- dashcam_empty_desc: "Driving routes and video segments will appear here after recording.",
- selected_count: "選択 {count}件",
- select_all: "すべて選択",
- deselect_all: "すべて解除",
- upload_selected: "選択を送信",
- segment_count: "セグメント {count}件",
- segment_menu: "セグメントメニュー",
- show_segments: "セグメント表示",
- collapse: "折りたたむ",
- log_upload: "ログ送信",
- log_upload_confirm: "Carrotサーバーに{count}件のログを送信しますか?",
- upload_data_warning: "This upload may use mobile data depending on your network connection.",
- upload_file_count: "{count} files",
- upload_files_unknown: "files unknown",
- upload_size_unknown: "size unknown",
- log_uploading: "ログ送信中",
- log_upload_result: "ログ送信結果",
- upload_count: "アップロード {uploaded}/{total}",
- upload_complete_count: "送信完了 {uploaded}/{total}",
- upload_canceled: "送信をキャンセルしました。",
- upload_canceling: "キャンセル中...",
- upload_already_running: "すでにログ送信中です。",
- dashcam_load_failed: "ドライブ録画リストの読み込みに失敗しました",
- screenrecord_empty: "画面録画がありません。",
- screenrecord_empty_title: "No screen recordings",
- screenrecord_empty_desc: "Screen recording files will appear here after recording.",
- screenrecord_load_failed: "画面録画リストの読み込みに失敗しました",
- toggle_log_panel: "ログパネルを展開/折りたたみ",
- git_reset_head_prompt: "HEAD を基準にリセット方式を選択してください。",
- disable_dm_inactive: "DisableDM が 2 のとき使用できます。",
- disable_dm_check_failed: "DisableDM 状態を確認できません。",
- waiting_model: "modelV2待機中...",
- no_selected_segments: "選択されたセグメントがありません。",
- play: "再生",
- video_controls: "動画操作",
- rewind_5: "5秒戻る",
- forward_5: "5秒進む",
- pause: "一時停止",
- ended: "終了",
- muted: "ミュート",
- fullscreen: "全画面",
- fullscreen_exit: "全画面解除",
- pip_exit: "PiP 解除",
- git_remote_title: "リポジトリ変更",
- git_remote_prompt: "現在: {url}\n\n新しい GitHub リポジトリ URL を入力してください。\n(現在の接続を上書きします)",
- git_remote_fetching: "リポジトリ情報を取得しています。\n新しいリポジトリでは数分かかる場合があります。\nお待ちください...",
- git_remote_success: "リポジトリを変更しました。\n[change branch] でブランチを選択してください。",
- git_add_remote_title: "リモート追加/更新",
- git_add_remote_name_prompt: "リモート名を入力してください(例: remote)",
- git_add_remote_url_prompt: "'{name}' の URL を入力してください",
- git_add_remote_done: "リモート '{name}' を追加/更新しました",
- git_log_checkout_prompt: "チェックアウトするコミットを選択してください",
- git_log_checkout_confirm: "このコミットをチェックアウトしますか?",
- git_log_checkout_done: "チェックアウト完了",
- git_reset_repo_title: "リポジトリ初期化",
- git_reset_repo_confirm: "警告: origin を削除し、'ajouatom/openpilot' を再追加します。\nすべてのローカル変更が失われます。続行しますか?",
- git_reset_repo_no_branches: "ブランチが見つかりません",
- git_reset_repo_branch_message: "初期化先のブランチを選択してください",
- git_reset_repo_done: "'{branch}' へ初期化しました",
- reset_calib_title: "再キャリブレーション",
- reset_calib_confirm: "キャリブレーションをリセットしますか?\nデバイスは自動的に再起動します。",
- device_lang_select_prompt: "デバイス UI の言語を選択してください",
- setting_changed_reboot: "設定を変更しました。今すぐ再起動しますか?",
- settings_not_loaded: "設定を読み込めません",
- copy_settings_done: "{count} 個の Params をコピーしました",
- settings_title: "設定 ({count} 個の Params)",
- qr_backup: "QR バックアップ",
- qr_restore: "QR 復元",
- qr_backup_title: "QR バックアップ",
- qr_restore_title: "QR 復元",
- qr_backup_count: "{count} 個の Params",
- qr_backup_size: "{chars} 文字",
- qr_configuring: "しばらくお待ちください",
- qr_config_done: "QR 機能の構成が完了しました。",
- qr_config_failed: "QR 機能を構成できませんでした。",
- qr_restore_upload: "画像",
- qr_restore_camera: "カメラ",
- qr_restore_camera_disabled: "カメラ使用不可",
- qr_restore_stop_camera: "カメラ停止",
- qr_restore_paste_placeholder: "QR バックアップ文字列を貼り付け",
- qr_restore_check: "確認",
- qr_restore_hint: "カメラでスキャンするか、QR バックアップ画像を選択して復元してください。",
- qr_restore_scan_hint: "カメラを QR コードに向けてください。",
- qr_restore_scan_detected: "QR コードをガイド枠内に合わせてください。",
- qr_restore_scan_aligned: "QR コードが合いました。そのままお待ちください。",
- qr_restore_scan_locked: "QR コードを読み取りました。",
- qr_restore_decode_failed: "QR コードが見つかりません。",
- qr_restore_previewing: "バックアップを確認中...",
- qr_restore_ready: "{count} 件の変更を適用できます",
- qr_restore_no_changes: "適用する変更はありません。",
- qr_restore_apply: "適用",
- qr_restore_changed: "変更",
- qr_restore_current_value: "現在",
- qr_restore_backup_value: "復元",
- qr_restore_same: "同一",
- qr_restore_skipped: "スキップ",
- qr_restore_invalid: "無効",
- qr_restore_more: "他 {count} 件の変更があります",
- qr_restore_applied: "{count} 個の Params を復元しました",
- qr_restore_https_required: "カメラを使うには HTTPS で開いてください。",
- qr_restore_camera_unsupported: "このブラウザはリアルタイムカメラに対応していません。",
- qr_restore_camera_failed: "カメラを開けません。",
- qr_restore_preview_failed: "バックアップを読み取れません。",
- empty_value: "(空)",
- },
- actionLabels: {
- git_pull: { running: "更新を確認中...", done: "更新完了", failed: "更新失敗" },
- git_sync: { running: "ブランチを同期中...", done: "同期完了", failed: "同期失敗" },
- git_reset: { running: "リセット中...", done: "リセット完了", failed: "リセット失敗" },
- git_checkout: { running: "ブランチ切替中...", done: "ブランチ変更完了", failed: "ブランチ切替失敗" },
- git_branch_list: { running: "ブランチ読み込み中...", done: "ブランチ読み込み完了", failed: "読み込み失敗" },
- reboot: { running: "再起動を要求中...", done: "再起動開始", failed: "再起動失敗" },
- send_tmux_log: { running: "ログをダウンロード中...", done: "ダウンロード完了", failed: "ダウンロード失敗" },
- server_tmux_log: { running: "サーバーログを送信中...", done: "送信完了", failed: "送信失敗" },
- backup_settings: { running: "設定をバックアップ中...", done: "バックアップ完了", failed: "バックアップ失敗" },
- delete_all_videos: { running: "動画を削除中...", done: "削除完了", failed: "削除失敗" },
- delete_all_logs: { running: "ログを削除中...", done: "削除完了", failed: "削除失敗" },
- rebuild_all: { running: "フルリビルド中...", done: "リビルドと再起動を開始", failed: "リビルド失敗" },
- shell_cmd: { running: "コマンド実行中...", done: "完了", failed: "コマンド失敗" },
- install_required: { running: "パッケージをインストール中...", done: "インストール完了", failed: "インストール失敗" },
- git_remote_add: { running: "リモート追加/更新中...", done: "リモート追加/更新完了", failed: "リモート追加/更新失敗" },
- git_log: { running: "コミット読み込み中...", done: "読み込み完了", failed: "読み込み失敗" },
- git_reset_repo_fetch: { running: "リポジトリ情報取得中...", done: "取得完了", failed: "取得失敗" },
- git_reset_repo_checkout: { running: "リポジトリ初期化中...", done: "初期化完了", failed: "初期化失敗" },
- reset_calib: { running: "キャリブレーションをリセット中...", done: "リセット完了", failed: "リセット失敗" },
- },
- errorMessages: {
- GIT_CMD_NOT_ALLOWED: (d) => `This git command is not allowed: ${d}`,
- CMD_NOT_ALLOWED: (d) => `This command is not allowed: ${d}`,
- INVALID_RESET_MODE: () => "Invalid reset mode",
- MISSING_BRANCH: () => "Please select a branch",
- CMD_TIMEOUT: () => "Command timed out",
- TMUX_CAPTURE_FAIL: () => "Failed to capture log",
- },
- driveModes: { normal: "Normal", eco: "Eco", safe: "Safe", sport: "Sport" },
-});
diff --git a/selfdrive/carrot/web/js/translations/ko.js b/selfdrive/carrot/web/js/translations/ko.js
index b5a083ead0..d1d72b3aa0 100644
--- a/selfdrive/carrot/web/js/translations/ko.js
+++ b/selfdrive/carrot/web/js/translations/ko.js
@@ -31,8 +31,8 @@ window.CarrotTranslations.register("ko", {
git_commands: "Git Commands",
user_system: "User / System",
reboot: "재부팅",
- backup: "설정 백업",
- restore: "설정 복원",
+ backup: "Backup",
+ restore: "Restore",
copy: "Copy",
view: "View",
device_info: "기기 정보",
@@ -246,6 +246,9 @@ window.CarrotTranslations.register("ko", {
input_title: "입력",
ok: "확인",
cancel: "취소",
+ delete: "삭제",
+ default_value: "기본값",
+ default_value_confirm: "{name} 값을 기본값({value})으로 되돌릴까요?",
quick_link_empty: "GithubUsername 없음",
open_carrotman_confirm: "{name}을 여시겠습니까?",
device_lang_changed: "기기 언어가 변경되었습니다.\n적용하려면 기기를 재부팅하세요.",
@@ -263,11 +266,48 @@ window.CarrotTranslations.register("ko", {
terminal_disconnected: "연결끊김",
terminal_unavailable: "터미널 접속 실패",
terminal_offline: "터미널 오프라인",
+ setting_action_menu: "설정 작업",
setting_search: "설정 검색",
+ profile_add: "프로필 추가",
+ setting_profiles: "프로필",
+ setting_profile_create_title: "프로필 추가",
+ setting_profile_create_prompt: "프로필 이름을 입력하세요.",
+ setting_profile_name: "프로필 이름",
+ setting_profile_created: "생성",
+ setting_profile_info: "정보",
+ setting_profile_info_empty: "프로필 정보가 없습니다",
+ setting_profile_value: "프로필",
+ setting_profile_apply_title: "프로필 적용",
+ setting_profile_apply_done: "프로필이 적용되었습니다",
+ setting_profile_apply_failed: "프로필 적용에 실패했습니다",
+ setting_profile_delete: "프로필 삭제",
+ setting_profile_delete_confirm: "이 프로필을 삭제할까요?\n{name}",
+ setting_profile_saved: "프로필이 저장되었습니다",
+ setting_profile_deleted: "프로필이 삭제되었습니다",
+ setting_profile_save_failed: "프로필 저장에 실패했습니다",
+ setting_profile_menu: "프로필 메뉴",
+ setting_profile_search: "프로필 검색",
+ setting_profile_search_placeholder: "이 프로필에서 검색",
+ setting_reset_defaults: "설정 초기화",
+ setting_reset_defaults_confirm: "전체 설정을 기본값으로 초기화할까요?",
+ setting_reset_defaults_done: "설정 초기화 성공",
+ setting_reset_defaults_failed: "설정 초기화 실패",
setting_search_placeholder: "이름, 설명, 그룹 검색",
setting_search_empty: "검색 결과가 없습니다.",
setting_search_idle: "검색어를 입력하면 세부 설정을 찾을 수 있습니다.",
setting_search_results: "검색 결과",
+ setting_search_all: "전체 설정",
+ setting_search_source_carrot: "당근파일럿",
+ setting_search_source_profile: "프로필",
+ settings_diff_changed: "변경",
+ settings_diff_same: "같음",
+ settings_diff_skipped: "제외",
+ settings_diff_invalid: "오류",
+ settings_diff_current: "현재",
+ settings_diff_apply: "적용",
+ settings_diff_changed_status: "변경",
+ settings_diff_no_changes: "적용할 변경사항이 없습니다.",
+ settings_diff_more: "{count}개 변경사항 더 있음",
logs_dashcam: "대시캠",
logs_screenrecord: "화면녹화",
display_mode: "표시 모드",
@@ -365,10 +405,10 @@ window.CarrotTranslations.register("ko", {
settings_not_loaded: "설정을 불러오지 못했습니다",
copy_settings_done: "{count}개 Params 복사됨",
settings_title: "설정 ({count}개 Params)",
- qr_backup: "QR 백업",
- qr_restore: "QR 복원",
- qr_backup_title: "QR 백업",
- qr_restore_title: "QR 복원",
+ qr_backup: "QR Backup",
+ qr_restore: "QR Restore",
+ qr_backup_title: "QR Backup",
+ qr_restore_title: "QR Restore",
qr_backup_count: "{count}개 Params",
qr_backup_size: "{chars}자",
qr_configuring: "잠시만 기다려주세요",
diff --git a/selfdrive/carrot/web/js/translations/registry.js b/selfdrive/carrot/web/js/translations/registry.js
index 6b29ce0b3e..b9385c87ca 100644
--- a/selfdrive/carrot/web/js/translations/registry.js
+++ b/selfdrive/carrot/web/js/translations/registry.js
@@ -3,7 +3,7 @@
(function initCarrotTranslations(global) {
const api = global.CarrotTranslations || {};
const packs = api.packs || {};
- const order = api.order || ["en", "ko", "zh", "ja", "fr"];
+ const order = api.order || ["en", "ko", "zh"];
const strings = api.strings || {};
const actionLabels = api.actionLabels || {};
const errorMessages = api.errorMessages || {};
diff --git a/selfdrive/carrot/web/js/translations/zh.js b/selfdrive/carrot/web/js/translations/zh.js
index 7d013727af..0f49a21a34 100644
--- a/selfdrive/carrot/web/js/translations/zh.js
+++ b/selfdrive/carrot/web/js/translations/zh.js
@@ -246,6 +246,9 @@ window.CarrotTranslations.register("zh", {
input_title: "输入",
ok: "确定",
cancel: "取消",
+ delete: "删除",
+ default_value: "默认值",
+ default_value_confirm: "将 {name} 恢复为默认值 ({value})?",
quick_link_empty: "GithubUsername 未设置",
open_carrotman_confirm: "打开 {name}?",
device_lang_changed: "设备语言已更改。\n请重启设备以应用。",
@@ -263,11 +266,48 @@ window.CarrotTranslations.register("zh", {
terminal_disconnected: "连接已断开",
terminal_unavailable: "终端不可用",
terminal_offline: "终端离线",
+ setting_action_menu: "设置操作",
setting_search: "设置搜索",
+ profile_add: "添加配置",
+ setting_profiles: "配置",
+ setting_profile_create_title: "添加配置",
+ setting_profile_create_prompt: "输入配置名称。",
+ setting_profile_name: "配置名称",
+ setting_profile_created: "创建时间",
+ setting_profile_info: "信息",
+ setting_profile_info_empty: "没有配置元数据",
+ setting_profile_value: "配置",
+ setting_profile_apply_title: "应用配置",
+ setting_profile_apply_done: "配置已应用",
+ setting_profile_apply_failed: "应用配置失败",
+ setting_profile_delete: "删除配置",
+ setting_profile_delete_confirm: "删除此配置?\n{name}",
+ setting_profile_saved: "配置已保存",
+ setting_profile_deleted: "配置已删除",
+ setting_profile_save_failed: "保存配置失败",
+ setting_profile_menu: "配置菜单",
+ setting_profile_search: "搜索配置",
+ setting_profile_search_placeholder: "在此配置中搜索",
+ setting_reset_defaults: "重置设置",
+ setting_reset_defaults_confirm: "将全部设置重置为默认值?",
+ setting_reset_defaults_done: "设置重置成功",
+ setting_reset_defaults_failed: "设置重置失败",
setting_search_placeholder: "搜索名称、描述、分组",
setting_search_empty: "没有匹配的设置项。",
setting_search_idle: "输入关键词以查找详细设置。",
setting_search_results: "项结果",
+ setting_search_all: "全部设置",
+ setting_search_source_carrot: "CarrotPilot",
+ setting_search_source_profile: "配置",
+ settings_diff_changed: "已更改",
+ settings_diff_same: "相同",
+ settings_diff_skipped: "已跳过",
+ settings_diff_invalid: "无效",
+ settings_diff_current: "当前",
+ settings_diff_apply: "应用",
+ settings_diff_changed_status: "已更改",
+ settings_diff_no_changes: "没有可应用的更改。",
+ settings_diff_more: "还有 {count} 个更改未显示",
logs_dashcam: "行车记录",
logs_screenrecord: "屏幕录制",
display_mode: "显示模式",