remcodex 0.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +331 -0
- package/dist/server/src/app.js +186 -0
- package/dist/server/src/cli.js +270 -0
- package/dist/server/src/controllers/codex-options.controller.js +199 -0
- package/dist/server/src/controllers/message.controller.js +21 -0
- package/dist/server/src/controllers/project.controller.js +44 -0
- package/dist/server/src/controllers/session.controller.js +175 -0
- package/dist/server/src/db/client.js +10 -0
- package/dist/server/src/db/migrations.js +32 -0
- package/dist/server/src/gateways/ws.gateway.js +60 -0
- package/dist/server/src/services/codex-app-server-runner.js +363 -0
- package/dist/server/src/services/codex-exec-runner.js +147 -0
- package/dist/server/src/services/codex-rollout-sync.js +977 -0
- package/dist/server/src/services/codex-runner.js +11 -0
- package/dist/server/src/services/codex-stream-events.js +478 -0
- package/dist/server/src/services/event-store.js +328 -0
- package/dist/server/src/services/project-manager.js +130 -0
- package/dist/server/src/services/pty-runner.js +72 -0
- package/dist/server/src/services/session-manager.js +1586 -0
- package/dist/server/src/services/session-timeline-service.js +181 -0
- package/dist/server/src/types/codex-launch.js +2 -0
- package/dist/server/src/types/models.js +37 -0
- package/dist/server/src/utils/ansi.js +143 -0
- package/dist/server/src/utils/codex-launch.js +102 -0
- package/dist/server/src/utils/codex-quota.js +179 -0
- package/dist/server/src/utils/codex-status.js +163 -0
- package/dist/server/src/utils/codex-ui-options.js +114 -0
- package/dist/server/src/utils/command.js +46 -0
- package/dist/server/src/utils/errors.js +16 -0
- package/dist/server/src/utils/ids.js +7 -0
- package/dist/server/src/utils/node-pty.js +29 -0
- package/package.json +36 -0
- package/scripts/fix-node-pty-helper.js +36 -0
- package/web/api.js +175 -0
- package/web/app.js +8082 -0
- package/web/components/composer.js +627 -0
- package/web/components/session-workbench.js +173 -0
- package/web/i18n/index.js +171 -0
- package/web/i18n/locales/de.js +50 -0
- package/web/i18n/locales/en.js +320 -0
- package/web/i18n/locales/es.js +50 -0
- package/web/i18n/locales/fr.js +50 -0
- package/web/i18n/locales/ja.js +50 -0
- package/web/i18n/locales/ko.js +50 -0
- package/web/i18n/locales/pt-BR.js +50 -0
- package/web/i18n/locales/ru.js +50 -0
- package/web/i18n/locales/zh-CN.js +320 -0
- package/web/i18n/locales/zh-Hant.js +53 -0
- package/web/index.html +23 -0
- package/web/message-rich-text.js +218 -0
- package/web/session-command-activity.js +980 -0
- package/web/session-event-adapter.js +826 -0
- package/web/session-timeline-reducer.js +728 -0
- package/web/session-timeline-renderer.js +656 -0
- package/web/session-ws.js +31 -0
- package/web/styles.css +5665 -0
- package/web/vendor/markdown-it.js +6969 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { t } from "../i18n/index.js";
|
|
2
|
+
|
|
3
|
+
function escapeHtml(value) {
|
|
4
|
+
return String(value ?? "")
|
|
5
|
+
.replaceAll("&", "&")
|
|
6
|
+
.replaceAll("<", "<")
|
|
7
|
+
.replaceAll(">", ">")
|
|
8
|
+
.replaceAll('"', """)
|
|
9
|
+
.replaceAll("'", "'");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getCompactMobileTitle(title) {
|
|
13
|
+
const text = String(title || "").trim();
|
|
14
|
+
if (!text) {
|
|
15
|
+
return t("session.current");
|
|
16
|
+
}
|
|
17
|
+
return text.length > 10 ? `${text.slice(0, 10)}…` : text;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function renderSessionTopBar({
|
|
21
|
+
title,
|
|
22
|
+
statusCode = "",
|
|
23
|
+
statusLabel,
|
|
24
|
+
statusClass,
|
|
25
|
+
activityBadges = [],
|
|
26
|
+
host,
|
|
27
|
+
model,
|
|
28
|
+
reasoning,
|
|
29
|
+
sessionElapsedLabel,
|
|
30
|
+
activeElapsedLabel,
|
|
31
|
+
inspectOpen,
|
|
32
|
+
showInspectAction = true,
|
|
33
|
+
backHref = "",
|
|
34
|
+
}) {
|
|
35
|
+
const mobilePrimaryLabel =
|
|
36
|
+
statusCode === "waiting_input" || statusCode === "idle" || statusCode === "completed"
|
|
37
|
+
? getCompactMobileTitle(title)
|
|
38
|
+
: statusLabel || t("session.status.unknown");
|
|
39
|
+
|
|
40
|
+
return `
|
|
41
|
+
<section class="session-topbar" aria-label="${escapeHtml(t("generic.status"))}">
|
|
42
|
+
<div class="session-topbar-mobile-bar">
|
|
43
|
+
${
|
|
44
|
+
backHref
|
|
45
|
+
? `<a href="${escapeHtml(backHref)}" class="session-topbar-mobile-back">${escapeHtml(t("session.back"))}</a>`
|
|
46
|
+
: `<span class="session-topbar-mobile-back session-topbar-mobile-back-placeholder"></span>`
|
|
47
|
+
}
|
|
48
|
+
<div class="session-topbar-mobile-center">
|
|
49
|
+
<div class="session-topbar-mobile-status-row">
|
|
50
|
+
<div class="session-topbar-mobile-status">${escapeHtml(mobilePrimaryLabel)}</div>
|
|
51
|
+
${
|
|
52
|
+
activeElapsedLabel
|
|
53
|
+
? `<div id="session-mobile-active-elapsed" class="session-topbar-mobile-elapsed">${escapeHtml(activeElapsedLabel)}</div>`
|
|
54
|
+
: ""
|
|
55
|
+
}
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
${
|
|
59
|
+
showInspectAction
|
|
60
|
+
? `
|
|
61
|
+
<button
|
|
62
|
+
id="inspect-drawer-toggle"
|
|
63
|
+
type="button"
|
|
64
|
+
class="secondary-button session-topbar-mobile-action ${inspectOpen ? "session-topbar-action-active" : ""}"
|
|
65
|
+
aria-expanded="${inspectOpen ? "true" : "false"}"
|
|
66
|
+
aria-controls="inspect-drawer"
|
|
67
|
+
>
|
|
68
|
+
Inspect
|
|
69
|
+
</button>
|
|
70
|
+
`
|
|
71
|
+
: `<span class="session-topbar-mobile-action session-topbar-mobile-action-placeholder"></span>`
|
|
72
|
+
}
|
|
73
|
+
</div>
|
|
74
|
+
<div class="session-topbar-main">
|
|
75
|
+
<h2 class="session-topbar-title">${escapeHtml(title || t("workspace.session.untitled"))}</h2>
|
|
76
|
+
<div class="session-topbar-statuses">
|
|
77
|
+
<span class="pill ${escapeHtml(statusClass || "pill-neutral")}">${escapeHtml(statusLabel || t("session.status.unknown"))}</span>
|
|
78
|
+
${activityBadges
|
|
79
|
+
.map(
|
|
80
|
+
(badge) => `
|
|
81
|
+
<span class="session-topbar-live-badge session-topbar-live-badge-${escapeHtml(badge.tone || "neutral")}">
|
|
82
|
+
${escapeHtml(badge.label || "")}
|
|
83
|
+
</span>
|
|
84
|
+
`,
|
|
85
|
+
)
|
|
86
|
+
.join("")}
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
<div class="session-topbar-meta">
|
|
90
|
+
<span class="session-topbar-chip">${escapeHtml(host || t("session.host.unsynced"))}</span>
|
|
91
|
+
<span class="session-topbar-chip">${escapeHtml(model || t("session.model.unsynced"))}</span>
|
|
92
|
+
<span class="session-topbar-chip">${escapeHtml(reasoning || t("session.reasoning.unsynced"))}</span>
|
|
93
|
+
<span id="session-elapsed-chip" class="session-topbar-chip">
|
|
94
|
+
${escapeHtml(sessionElapsedLabel || t("session.elapsed", { value: "--" }))}
|
|
95
|
+
</span>
|
|
96
|
+
${
|
|
97
|
+
activeElapsedLabel
|
|
98
|
+
? `
|
|
99
|
+
<span id="session-active-elapsed-chip" class="session-topbar-chip session-topbar-chip-active">
|
|
100
|
+
${escapeHtml(activeElapsedLabel)}
|
|
101
|
+
</span>
|
|
102
|
+
`
|
|
103
|
+
: ""
|
|
104
|
+
}
|
|
105
|
+
</div>
|
|
106
|
+
${
|
|
107
|
+
showInspectAction
|
|
108
|
+
? `
|
|
109
|
+
<button
|
|
110
|
+
id="inspect-drawer-toggle"
|
|
111
|
+
type="button"
|
|
112
|
+
class="secondary-button session-topbar-action ${inspectOpen ? "session-topbar-action-active" : ""}"
|
|
113
|
+
aria-expanded="${inspectOpen ? "true" : "false"}"
|
|
114
|
+
aria-controls="inspect-drawer"
|
|
115
|
+
>
|
|
116
|
+
Inspect
|
|
117
|
+
</button>
|
|
118
|
+
`
|
|
119
|
+
: ""
|
|
120
|
+
}
|
|
121
|
+
</section>
|
|
122
|
+
`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function renderInspectDrawer({
|
|
126
|
+
open,
|
|
127
|
+
selectionTitle,
|
|
128
|
+
searchSectionHtml,
|
|
129
|
+
detailsSectionHtml,
|
|
130
|
+
sessionSectionHtml,
|
|
131
|
+
}) {
|
|
132
|
+
return `
|
|
133
|
+
<div
|
|
134
|
+
id="inspect-drawer-overlay"
|
|
135
|
+
class="inspect-drawer-overlay ${open ? "inspect-drawer-overlay-open" : ""}"
|
|
136
|
+
${open ? "" : "hidden"}
|
|
137
|
+
></div>
|
|
138
|
+
<aside
|
|
139
|
+
id="inspect-drawer"
|
|
140
|
+
class="inspect-drawer ${open ? "inspect-drawer-open" : ""}"
|
|
141
|
+
aria-label="Inspect"
|
|
142
|
+
${open ? "" : "hidden"}
|
|
143
|
+
>
|
|
144
|
+
<div class="inspect-drawer-head">
|
|
145
|
+
<div>
|
|
146
|
+
<p class="inspect-drawer-eyebrow">Inspect</p>
|
|
147
|
+
<h3 class="inspect-drawer-title">${escapeHtml(selectionTitle || t("inspect.selectionTitle"))}</h3>
|
|
148
|
+
</div>
|
|
149
|
+
<button id="inspect-drawer-close" type="button" class="secondary-button">${escapeHtml(t("inspect.close"))}</button>
|
|
150
|
+
</div>
|
|
151
|
+
<div class="inspect-drawer-body">
|
|
152
|
+
<section class="inspect-drawer-section">
|
|
153
|
+
<div class="inspect-section-head">
|
|
154
|
+
<span class="inspect-section-title">Search</span>
|
|
155
|
+
</div>
|
|
156
|
+
${searchSectionHtml}
|
|
157
|
+
</section>
|
|
158
|
+
<section class="inspect-drawer-section">
|
|
159
|
+
<div class="inspect-section-head">
|
|
160
|
+
<span class="inspect-section-title">Details</span>
|
|
161
|
+
</div>
|
|
162
|
+
${detailsSectionHtml}
|
|
163
|
+
</section>
|
|
164
|
+
<section class="inspect-drawer-section">
|
|
165
|
+
<div class="inspect-section-head">
|
|
166
|
+
<span class="inspect-section-title">Session</span>
|
|
167
|
+
</div>
|
|
168
|
+
${sessionSectionHtml}
|
|
169
|
+
</section>
|
|
170
|
+
</div>
|
|
171
|
+
</aside>
|
|
172
|
+
`;
|
|
173
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import en from "./locales/en.js";
|
|
2
|
+
import de from "./locales/de.js";
|
|
3
|
+
import es from "./locales/es.js";
|
|
4
|
+
import fr from "./locales/fr.js";
|
|
5
|
+
import ja from "./locales/ja.js";
|
|
6
|
+
import ko from "./locales/ko.js";
|
|
7
|
+
import ptBR from "./locales/pt-BR.js";
|
|
8
|
+
import ru from "./locales/ru.js";
|
|
9
|
+
import zhCN from "./locales/zh-CN.js";
|
|
10
|
+
import zhHant from "./locales/zh-Hant.js";
|
|
11
|
+
|
|
12
|
+
const LOCALE_STORAGE_KEY = "remcodex.locale";
|
|
13
|
+
const LOCALE_OPTIONS = [
|
|
14
|
+
{ id: "en", label: "English" },
|
|
15
|
+
{ id: "zh-CN", label: "简体中文" },
|
|
16
|
+
{ id: "zh-Hant", label: "繁體中文" },
|
|
17
|
+
{ id: "ja", label: "日本語" },
|
|
18
|
+
{ id: "ko", label: "한국어" },
|
|
19
|
+
{ id: "es", label: "Español" },
|
|
20
|
+
{ id: "fr", label: "Français" },
|
|
21
|
+
{ id: "de", label: "Deutsch" },
|
|
22
|
+
{ id: "pt-BR", label: "Português (Brasil)" },
|
|
23
|
+
{ id: "ru", label: "Русский" },
|
|
24
|
+
];
|
|
25
|
+
const SUPPORTED_LOCALES = new Set(LOCALE_OPTIONS.map((item) => item.id));
|
|
26
|
+
const DICTIONARIES = {
|
|
27
|
+
de,
|
|
28
|
+
en,
|
|
29
|
+
es,
|
|
30
|
+
fr,
|
|
31
|
+
ja,
|
|
32
|
+
ko,
|
|
33
|
+
"pt-BR": ptBR,
|
|
34
|
+
ru,
|
|
35
|
+
"zh-CN": zhCN,
|
|
36
|
+
"zh-Hant": zhHant,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function normalizeLocale(value) {
|
|
40
|
+
const raw = String(value || "").trim().toLowerCase();
|
|
41
|
+
if (!raw) {
|
|
42
|
+
return "en";
|
|
43
|
+
}
|
|
44
|
+
if (raw === "zh" || raw === "zh-cn" || raw === "zh-hans" || raw.startsWith("zh-cn")) {
|
|
45
|
+
return "zh-CN";
|
|
46
|
+
}
|
|
47
|
+
if (
|
|
48
|
+
raw === "zh-tw" ||
|
|
49
|
+
raw === "zh-hk" ||
|
|
50
|
+
raw === "zh-hant" ||
|
|
51
|
+
raw.startsWith("zh-tw") ||
|
|
52
|
+
raw.startsWith("zh-hk") ||
|
|
53
|
+
raw.startsWith("zh-hant")
|
|
54
|
+
) {
|
|
55
|
+
return "zh-Hant";
|
|
56
|
+
}
|
|
57
|
+
if (raw === "ja" || raw.startsWith("ja-")) {
|
|
58
|
+
return "ja";
|
|
59
|
+
}
|
|
60
|
+
if (raw === "ko" || raw.startsWith("ko-")) {
|
|
61
|
+
return "ko";
|
|
62
|
+
}
|
|
63
|
+
if (raw === "es" || raw.startsWith("es-")) {
|
|
64
|
+
return "es";
|
|
65
|
+
}
|
|
66
|
+
if (raw === "fr" || raw.startsWith("fr-")) {
|
|
67
|
+
return "fr";
|
|
68
|
+
}
|
|
69
|
+
if (raw === "de" || raw.startsWith("de-")) {
|
|
70
|
+
return "de";
|
|
71
|
+
}
|
|
72
|
+
if (raw === "pt" || raw === "pt-br" || raw.startsWith("pt-br")) {
|
|
73
|
+
return "pt-BR";
|
|
74
|
+
}
|
|
75
|
+
if (raw === "ru" || raw.startsWith("ru-")) {
|
|
76
|
+
return "ru";
|
|
77
|
+
}
|
|
78
|
+
return "en";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function readStoredLocale() {
|
|
82
|
+
try {
|
|
83
|
+
const value = window.localStorage?.getItem(LOCALE_STORAGE_KEY);
|
|
84
|
+
return value && SUPPORTED_LOCALES.has(value) ? value : "";
|
|
85
|
+
} catch {
|
|
86
|
+
return "";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function detectLocale() {
|
|
91
|
+
const stored = readStoredLocale();
|
|
92
|
+
if (stored) {
|
|
93
|
+
return stored;
|
|
94
|
+
}
|
|
95
|
+
if (typeof navigator !== "undefined") {
|
|
96
|
+
return normalizeLocale(navigator.language || navigator.languages?.[0] || "");
|
|
97
|
+
}
|
|
98
|
+
return "en";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let currentLocale = detectLocale();
|
|
102
|
+
|
|
103
|
+
function interpolate(template, params = {}) {
|
|
104
|
+
return String(template).replace(/\{(\w+)\}/g, (_, key) => {
|
|
105
|
+
const value = params[key];
|
|
106
|
+
return value === undefined || value === null ? "" : String(value);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function resolveValue(dict, key) {
|
|
111
|
+
return dict && Object.prototype.hasOwnProperty.call(dict, key) ? dict[key] : undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function getCurrentLocale() {
|
|
115
|
+
return currentLocale;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function getIntlLocale() {
|
|
119
|
+
switch (currentLocale) {
|
|
120
|
+
case "zh-CN":
|
|
121
|
+
return "zh-Hans-CN";
|
|
122
|
+
case "zh-Hant":
|
|
123
|
+
return "zh-Hant";
|
|
124
|
+
case "pt-BR":
|
|
125
|
+
return "pt-BR";
|
|
126
|
+
default:
|
|
127
|
+
return currentLocale;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function listSupportedLocales() {
|
|
132
|
+
return LOCALE_OPTIONS;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function setCurrentLocale(locale) {
|
|
136
|
+
currentLocale = normalizeLocale(locale);
|
|
137
|
+
try {
|
|
138
|
+
window.localStorage?.setItem(LOCALE_STORAGE_KEY, currentLocale);
|
|
139
|
+
} catch {
|
|
140
|
+
/* ignore */
|
|
141
|
+
}
|
|
142
|
+
return currentLocale;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function toggleLocale() {
|
|
146
|
+
return setCurrentLocale(currentLocale === "zh-CN" ? "en" : "zh-CN");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function t(key, params = {}) {
|
|
150
|
+
const dict = DICTIONARIES[currentLocale] || DICTIONARIES.en;
|
|
151
|
+
const fallback = DICTIONARIES["zh-CN"];
|
|
152
|
+
const value = resolveValue(dict, key) ?? resolveValue(fallback, key) ?? resolveValue(DICTIONARIES.en, key);
|
|
153
|
+
if (typeof value === "function") {
|
|
154
|
+
return String(value(params));
|
|
155
|
+
}
|
|
156
|
+
if (typeof value === "string") {
|
|
157
|
+
return interpolate(value, params);
|
|
158
|
+
}
|
|
159
|
+
return key;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function formatInlineList(values) {
|
|
163
|
+
const list = Array.isArray(values) ? values.filter(Boolean) : [];
|
|
164
|
+
if (list.length === 0) {
|
|
165
|
+
return "";
|
|
166
|
+
}
|
|
167
|
+
if (currentLocale === "zh-CN") {
|
|
168
|
+
return list.join("、");
|
|
169
|
+
}
|
|
170
|
+
return list.join(", ");
|
|
171
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import en from "./en.js";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
...en,
|
|
5
|
+
"nav.projects": "Projekte",
|
|
6
|
+
"nav.sessions": "Sitzungen",
|
|
7
|
+
"workspace.openSidebar": "Sitzungsleiste öffnen",
|
|
8
|
+
"workspace.closeSidebar": "Sitzungsleiste ausblenden",
|
|
9
|
+
"workspace.empty.title": "Wähle eine Sitzung, um zu starten",
|
|
10
|
+
"workspace.empty.subtitle": "Wechsle Sitzungen in der Seitenleiste oder erstelle / importiere eine neue.",
|
|
11
|
+
"workspace.empty.newSession": "Neue Sitzung",
|
|
12
|
+
"workspace.sidebar.import": "Codex-Sitzung importieren",
|
|
13
|
+
"workspace.sidebar.newSession": "+ Neue Sitzung",
|
|
14
|
+
"workspace.sidebar.empty": "Keine Sitzung passt zum aktuellen Filter.",
|
|
15
|
+
"workspace.session.untitled": "Unbenannte Sitzung",
|
|
16
|
+
"workspace.loading.session": "Sitzungsinhalt wird geladen...",
|
|
17
|
+
"workspace.loading.projects": "Projekte werden geladen...",
|
|
18
|
+
"workspace.loading.sessions": "Sitzungen werden geladen...",
|
|
19
|
+
"session.status.idle": "Leerlauf",
|
|
20
|
+
"session.status.starting": "Startet",
|
|
21
|
+
"session.status.running": "Läuft",
|
|
22
|
+
"session.status.waiting_input": "Wartet",
|
|
23
|
+
"session.status.stopping": "Wird gestoppt",
|
|
24
|
+
"session.status.completed": "Abgeschlossen",
|
|
25
|
+
"session.status.failed": "Fehlgeschlagen",
|
|
26
|
+
"session.current": "Aktuelle Sitzung",
|
|
27
|
+
"approval.required": "Genehmigung erforderlich",
|
|
28
|
+
"approval.pending": "Ausstehend",
|
|
29
|
+
"approval.deny": "Ablehnen",
|
|
30
|
+
"approval.allowOnce": "Einmal erlauben",
|
|
31
|
+
"approval.allowForTurn": "Für diesen Zug erlauben",
|
|
32
|
+
"composer.placeholder": "Beschreibe die Entwicklungsaufgabe, die Codex übernehmen soll",
|
|
33
|
+
"timeline.empty": "Noch keine Unterhaltung.",
|
|
34
|
+
"timeline.thinking": "Denkt nach...",
|
|
35
|
+
"generic.close": "Schließen",
|
|
36
|
+
"generic.back": "Zurück",
|
|
37
|
+
"generic.refresh": "Aktualisieren",
|
|
38
|
+
"generic.search": "Suchen",
|
|
39
|
+
"generic.project": "Projekt",
|
|
40
|
+
"generic.status": "Status",
|
|
41
|
+
"generic.sort": "Sortieren",
|
|
42
|
+
"generic.keyword": "Suchbegriff",
|
|
43
|
+
"generic.copy": "Kopieren",
|
|
44
|
+
"generic.expand": "Erweitern",
|
|
45
|
+
"generic.collapse": "Einklappen",
|
|
46
|
+
"generic.on": "An",
|
|
47
|
+
"generic.off": "Aus",
|
|
48
|
+
"generic.none": "Keine",
|
|
49
|
+
"inspect.selectionTitle": "Details",
|
|
50
|
+
};
|