Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
0dd2958
docs: add Turkish README translation (README.tr.md)
ardakrt Aug 7, 2026
e0d156f
feat(gui): add Turkish translation for Web Dashboard UI
ardakrt Aug 7, 2026
0df9f3a
test(gui): add regression unit test for Turkish i18n locale
ardakrt Aug 7, 2026
7f38b58
docs(tr): resolve CodeRabbit review findings in README.tr.md
ardakrt Aug 7, 2026
4badb9e
test(gui): strengthen detectInitial test assertion for Turkish locale
ardakrt Aug 7, 2026
b27400a
fix(gui): address Codex review feedback for Turkish locale, formatter…
ardakrt Aug 7, 2026
ea277d1
fix(gui): resolve Codex review points for status codes, duplicate key…
ardakrt Aug 7, 2026
92ffd95
fix(gui): address CodeRabbit review suggestions for uptime formatting…
ardakrt Aug 7, 2026
d705d48
fix(gui): move uptime labels to shared i18n locale catalog
ardakrt Aug 7, 2026
105715b
ci: trigger fresh CI run for PR #1235
ardakrt Aug 8, 2026
1d0a864
fix(gui): address Codex review points for translation precision and r…
ardakrt Aug 8, 2026
9f122ea
Merge remote-tracking branch 'origin/dev' into docs/add-turkish-trans…
ardakrt Aug 8, 2026
1110cd6
fix(gui): sync upstream additions for prov.loginSameAccount and visio…
ardakrt Aug 8, 2026
cb9774b
Merge remote-tracking branch 'origin/dev' into docs/add-turkish-trans…
ardakrt Aug 9, 2026
025b219
fix(gui): address maintainer Wibias review points for test-safe descr…
ardakrt Aug 9, 2026
f47af1c
Merge remote-tracking branch 'origin/dev' into docs/add-turkish-trans…
ardakrt Aug 9, 2026
06fbc75
fix(gui,docs): refine V2 effort-cap, account-switch, round-robin, Cla…
ardakrt Aug 9, 2026
d895c70
fix(gui): route locale labels through i18n catalogs
Wibias Aug 9, 2026
5c60007
fix(gui): enforce shared locale catalog contracts
Wibias Aug 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ ocx start # proxy + dashboard on localhost:10100
</table>

<p align="center">
<a href="README.md">English</a> · <a href="readme/README.ko.md">한국어</a> · <a href="readme/README.zh-CN.md">简体中文</a> · <a href="readme/README.ru.md">Русский</a> · <a href="readme/README.ja.md">日本語</a> · 📖 <a href="https://opencodex.me/"><b>Full documentation →</b></a>
<a href="README.md">English</a> · <a href="readme/README.ko.md">한국어</a> · <a href="readme/README.zh-CN.md">简体中文</a> · <a href="readme/README.ru.md">Русский</a> · <a href="readme/README.ja.md">日本語</a> · <a href="readme/README.tr.md">Türkçe</a> · 📖 <a href="https://opencodex.me/"><b>Full documentation →</b></a>
</p>

opencodex is a lightweight local proxy that translates Codex's Responses API into whatever your
Expand Down
4 changes: 2 additions & 2 deletions gui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import Startup from "./pages/Startup";
import ErrorBoundary from "./components/ErrorBoundary";
import { SidebarGithubRow } from "./components/sidebar-github-row";
import { IconGrid, IconServer, IconBoxes, IconBot, IconList, IconActivity, IconHardDrive, IconKey, IconMenu, IconSun, IconMoon, IconMonitor, IconGlobe, IconPower, IconX } from "./icons";
import { useI18n, useT, LOCALES, type Locale, type TKey } from "./i18n/shared";
import { useI18n, useT, LOCALES, localeDisplayName, type Locale, type TKey } from "./i18n/shared";
import { Select } from "./ui";
import { installApiAuthFetch } from "./api";
import { type Page } from "./app-routing";
Expand Down Expand Up @@ -252,7 +252,7 @@ export default function App() {
<IconGlobe aria-hidden />
<Select
value={locale}
options={LOCALES.map(l => ({ value: l.code, label: l.name }))}
options={LOCALES.map(l => ({ value: l.code, label: localeDisplayName(l.code) }))}
onChange={v => setLocale(v as Locale)}
label={t("lang.label")}
placement="right"
Expand Down
2 changes: 2 additions & 0 deletions gui/src/components/QuotaBars.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ function bcp47(locale: Locale): string {
return "ru-RU";
case "ja":
return "ja-JP";
case "tr":
return "tr-TR";
default: {
const _exhaustive: never = locale;
return _exhaustive;
Expand Down
24 changes: 9 additions & 15 deletions gui/src/formatUptime.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,24 @@
import type { Locale } from "./i18n/shared";

const UPTIME_UNITS: Record<Locale, { day: string; hour: string; minute: string; second: string }> = {
en: { day: "d", hour: "h", minute: "m", second: "s" },
de: { day: "T", hour: "Std", minute: "Min", second: "Sek" },
ko: { day: "일", hour: "시간", minute: "분", second: "초" },
zh: { day: "天", hour: "小时", minute: "分钟", second: "秒" },
ru: { day: "д", hour: "ч", minute: "мин", second: "с" },
ja: { day: "日", hour: "時間", minute: "分", second: "秒" },
};
import { catalogValue, type Locale } from "./i18n/catalogs";

export function formatUptime(seconds: number, locale: Locale): string {
const totalSeconds = Math.max(0, Math.floor(seconds));
const units = UPTIME_UNITS[locale] ?? UPTIME_UNITS.en;
const day = catalogValue(locale, "uptime.day");
const hour = catalogValue(locale, "uptime.hour");
const minute = catalogValue(locale, "uptime.minute");
const second = catalogValue(locale, "uptime.second");

if (totalSeconds < 5 * 60) return `${totalSeconds}${units.second}`;
if (totalSeconds < 5 * 60) return `${totalSeconds}${second}`;

const totalMinutes = Math.floor(totalSeconds / 60);
if (totalMinutes < 60) return `${totalMinutes}${units.minute}`;
if (totalMinutes < 60) return `${totalMinutes}${minute}`;

const totalHours = Math.floor(totalMinutes / 60);
if (totalHours < 24) {
const minutes = totalMinutes % 60;
return minutes > 0 ? `${totalHours}${units.hour} ${minutes}${units.minute}` : `${totalHours}${units.hour}`;
return minutes > 0 ? `${totalHours}${hour} ${minutes}${minute}` : `${totalHours}${hour}`;
}

const days = Math.floor(totalHours / 24);
const hours = totalHours % 24;
return hours > 0 ? `${days}${units.day} ${hours}${units.hour}` : `${days}${units.day}`;
return hours > 0 ? `${days}${day} ${hours}${hour}` : `${days}${day}`;
}
23 changes: 23 additions & 0 deletions gui/src/i18n/catalogs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { en, type TKey } from "./en";
import { de } from "./de";
import { ko } from "./ko";
import { zh } from "./zh";
import { ru } from "./ru";
import { ja } from "./ja";
import { tr } from "./tr";

/** React-free locale catalog registry for formatters and other shared helpers. */
export type Locale = "en" | "de" | "ko" | "zh" | "ru" | "ja" | "tr";

export const DICTS: Record<Locale, Record<TKey, string>> = { en, de, ko, zh, ru, ja, tr };

/** Native language names shown by the language picker, kept inside i18n rather than UI metadata. */
export function localeDisplayName(locale: Locale): string {
return DICTS[locale]["lang.nativeName"];
}

export function catalogValue(locale: Locale, key: TKey): string {
return DICTS[locale][key];
}

export type { TKey };
9 changes: 7 additions & 2 deletions gui/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import type { TKey } from "./en";
*/
export const de: Record<TKey, string> = {
"nav.dashboard": "Übersicht",
"uptime.day": "T",
"uptime.hour": "Std",
"uptime.minute": "Min",
"uptime.second": "Sek",
"nav.startup": "Startsicherheit",
"nav.providers": "Anbieter",
"nav.models": "Modelle",
Expand Down Expand Up @@ -84,6 +88,7 @@ export const de: Record<TKey, string> = {
"theme.dark": "Dunkel",
"theme.system": "System",
"lang.label": "Sprache",
"lang.nativeName": "Deutsch",
"provider.name.commandCodeAuth": "Command Code - Auth",
"provider.name.commandCodeApi": "Command Code - API",
"provider.name.volcengine": "Volcengine Ark",
Expand Down Expand Up @@ -1693,8 +1698,8 @@ export const de: Record<TKey, string> = {
"modal.apiKeyPlaceholder": "sk-… (oder $ENV_VAR)",
"modal.defaultModelPlaceholder": "z. B. gpt-5.5",
"modal.baseUrlPlaceholder": "https://...",
"modal.baseUrlPlaceholderError": "Base-URL enthält einen ungelösten {Platzhalter}. Ersetze ihn durch deinen tatsächlichen Wert.",
"modal.baseUrlPlaceholderHint": "Ersetze den {Platzhalter} in der Base-URL durch deine tatsächliche Account-ID, bevor du hinzufügst.",
"modal.baseUrlPlaceholderError": "Base-URL enthält einen ungelösten {placeholder}. Ersetze ihn durch deinen tatsächlichen Wert.",
"modal.baseUrlPlaceholderHint": "Ersetze den {placeholder} in der Base-URL durch deine tatsächliche Account-ID, bevor du hinzufügst.",
"modal.adding": "Wird hinzugefügt…",
"modal.useOauthLogin": "← OAuth-Login verwenden",
"codexAuth.addIdPlaceholder": "codex-work, codex-alt, team…",
Expand Down
5 changes: 5 additions & 0 deletions gui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
export const en = {
// sidebar / nav / common
"nav.dashboard": "Dashboard",
"uptime.day": "d",
"uptime.hour": "h",
"uptime.minute": "m",
"uptime.second": "s",
"nav.startup": "Startup",
"nav.providers": "Providers",
"nav.models": "Models",
Expand Down Expand Up @@ -44,6 +48,7 @@ export const en = {
"theme.dark": "Dark",
"theme.system": "System",
"lang.label": "Language",
"lang.nativeName": "English",
"provider.name.commandCodeAuth": "Command Code - Auth",
"provider.name.commandCodeApi": "Command Code - API",
"provider.name.volcengine": "Volcengine Ark",
Expand Down
5 changes: 5 additions & 0 deletions gui/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import type { TKey } from "./en";
export const ja: Record<TKey, string> = {
// sidebar / nav / common
"nav.dashboard": "ダッシュボード",
"uptime.day": "日",
"uptime.hour": "時間",
"uptime.minute": "分",
"uptime.second": "秒",
"nav.startup": "起動安全性",
"nav.providers": "プロバイダー",
"nav.models": "モデル",
Expand Down Expand Up @@ -89,6 +93,7 @@ export const ja: Record<TKey, string> = {
"theme.dark": "ダーク",
"theme.system": "システム",
"lang.label": "言語",
"lang.nativeName": "日本語",
"provider.name.commandCodeAuth": "Command Code - Auth",
"provider.name.commandCodeApi": "Command Code - API",
"provider.name.volcengine": "Volcengine Ark",
Expand Down
5 changes: 5 additions & 0 deletions gui/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import type { TKey } from "./en";
export const ko: Record<TKey, string> = {
// sidebar / nav / common
"nav.dashboard": "대시보드",
"uptime.day": "일",
"uptime.hour": "시간",
"uptime.minute": "분",
"uptime.second": "초",
"nav.startup": "시작 안전성",
"nav.providers": "프로바이더",
"nav.models": "모델",
Expand Down Expand Up @@ -84,6 +88,7 @@ export const ko: Record<TKey, string> = {
"theme.dark": "다크",
"theme.system": "시스템",
"lang.label": "언어",
"lang.nativeName": "한국어",
"provider.name.commandCodeAuth": "Command Code - Auth",
"provider.name.commandCodeApi": "Command Code - API",
"provider.name.volcengine": "Volcengine Ark",
Expand Down
5 changes: 5 additions & 0 deletions gui/src/i18n/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import type { TKey } from "./en";
export const ru: Record<TKey, string> = {
// sidebar / nav / common
"nav.dashboard": "Дашборд",
"uptime.day": "д",
"uptime.hour": "ч",
"uptime.minute": "мин",
"uptime.second": "с",
"nav.startup": "Безопасность запуска",
"nav.providers": "Провайдеры",
"nav.models": "Модели",
Expand Down Expand Up @@ -89,6 +93,7 @@ export const ru: Record<TKey, string> = {
"theme.dark": "Тёмная",
"theme.system": "Системная",
"lang.label": "Язык",
"lang.nativeName": "Русский",
"provider.name.commandCodeAuth": "Command Code - Auth",
"provider.name.commandCodeApi": "Command Code - API",
"provider.name.volcengine": "Volcengine Ark",
Expand Down
32 changes: 13 additions & 19 deletions gui/src/i18n/shared.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,16 @@
import { createContext, useContext } from "react";
import { en, type TKey } from "./en";
import { de } from "./de";
import { ko } from "./ko";
import { zh } from "./zh";
import { ru } from "./ru";
import { ja } from "./ja";
import { DICTS, localeDisplayName, type Locale, type TKey } from "./catalogs";

export type Locale = "en" | "de" | "ko" | "zh" | "ru" | "ja";
export type { TKey };
export { DICTS, localeDisplayName, type Locale, type TKey };

export const DICTS: Record<Locale, Record<TKey, string>> = { en, de, ko, zh, ru, ja };

export const LOCALES: { code: Locale; name: string; htmlLang: string }[] = [
{ code: "en", name: "English", htmlLang: "en" },
{ code: "de", name: "Deutsch", htmlLang: "de" },
{ code: "ko", name: "한국어", htmlLang: "ko" },
{ code: "zh", name: "中文", htmlLang: "zh-CN" },
{ code: "ru", name: "Русский", htmlLang: "ru" },
{ code: "ja", name: "日本語", htmlLang: "ja" },
export const LOCALES: { code: Locale; htmlLang: string }[] = [
{ code: "en", htmlLang: "en" },
{ code: "de", htmlLang: "de" },
{ code: "ko", htmlLang: "ko" },
{ code: "zh", htmlLang: "zh-CN" },
{ code: "ru", htmlLang: "ru" },
{ code: "ja", htmlLang: "ja" },
{ code: "tr", htmlLang: "tr" },
];

const LANG_KEY = "ocx-lang";
Expand All @@ -27,14 +20,15 @@ let activeLocale: Locale | null = null;
export function detectInitial(): Locale {
try {
const stored = localStorage.getItem(LANG_KEY);
if (stored === "en" || stored === "de" || stored === "ko" || stored === "zh" || stored === "ru" || stored === "ja") return stored;
if (stored === "en" || stored === "de" || stored === "ko" || stored === "zh" || stored === "ru" || stored === "ja" || stored === "tr") return stored;
} catch { /* ignore */ }
const nav = typeof navigator !== "undefined" ? navigator.language.toLowerCase() : "en";
const nav = typeof navigator !== "undefined" && navigator?.language ? navigator.language.toLowerCase() : "en";
if (nav.startsWith("de")) return "de";
if (nav.startsWith("ko")) return "ko";
if (nav.startsWith("zh")) return "zh";
if (nav.startsWith("ru")) return "ru";
if (nav.startsWith("ja")) return "ja";
if (nav.startsWith("tr")) return "tr";
return "en";
}

Expand Down
Loading
Loading