Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 9 additions & 1 deletion packages/ui/src/api/mePreferences.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ApiError } from "./errors";
import { createApiClient } from "./client";
import type { components } from "./schema";

Expand All @@ -7,7 +8,14 @@ export type UserPreferencesPatch = components["schemas"]["UserPreferencesPatch"]
export const createMePreferencesApi = (client: ReturnType<typeof createApiClient>) => {
return {
getPreferences: async (): Promise<User> => {
return client.request<User>({ path: "/me/preferences" });
try {
return await client.request<User>({ path: "/me/preferences" });
} catch (error) {
if (error instanceof ApiError && error.status === 404) {
return client.request<User>({ path: "/me" });
}
throw error;
}
},
setPreferences: async (payload: UserPreferencesPatch): Promise<User> => {
return client.request<User>({
Expand Down
7 changes: 6 additions & 1 deletion packages/ui/src/app/AdminAppearancePage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useCallback, useMemo, useState } from "react";
import { Button, ErrorState, LoadingSkeleton, PageHeader, Toolbar } from "@nimbus/ui-kit";
import type { I18nKey } from "../i18n/t";
import { t } from "../i18n/t";
import { setLocale, t } from "../i18n/t";
import { getAuthenticatedApiClient } from "./authenticatedApiClient";
import { createMePreferencesApi } from "../api/mePreferences";
import type { components } from "../api/schema";
Expand Down Expand Up @@ -63,6 +63,7 @@ export default function AdminAppearancePage() {
try {
const me = await api.getPreferences();

setLocale(me.locale);
const profile: AppearanceConfig = {
locale: me.locale,
theme: themeRef.current,
Expand All @@ -83,9 +84,13 @@ export default function AdminAppearancePage() {

try {
await api.setPreferences({ locale: draft.locale });
setLocale(draft.locale);
toggleTheme(draft.theme);
setCurrent(draft);
setMessageKey("admin.appearance.saved");
if (typeof window !== "undefined") {
window.setTimeout(() => window.location.reload(), 120);
}
} catch {
setErrorKey("admin.appearance.saveError");
} finally {
Expand Down
37 changes: 37 additions & 0 deletions packages/ui/src/app/AdminShell.css
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,40 @@
color: var(--nd-color-accent-default);
font-weight: 600;
}

@media (max-width: 900px) {
.admin-shell {
flex-direction: column;
}

.admin-shell__sidebar {
width: 100%;
border-right: none;
border-bottom: 1px solid var(--nd-color-border-default);
padding: 10px;
gap: 8px;
}

.admin-shell__title,
.admin-shell__subtitle {
font-size: 11px;
}

.admin-shell__nav {
flex-direction: row;
flex-wrap: nowrap;
gap: 6px;
overflow-x: auto;
padding-bottom: 2px;
}

.admin-shell__link {
margin-bottom: 0;
white-space: nowrap;
flex: 0 0 auto;
}

.admin-shell__content {
padding: 12px;
}
}
3 changes: 3 additions & 0 deletions packages/ui/src/app/AppShell.css
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@
color: var(--nd-color-text-primary);
margin-right: var(--nd-space-4);
white-space: nowrap;
text-decoration: none;
display: inline-flex;
align-items: center;
}

.app-shell__search {
Expand Down
4 changes: 3 additions & 1 deletion packages/ui/src/app/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,9 @@ export function AppShell() {
<header className="app-shell__topbar">
<div className="app-shell__topbar-left">

<div className="app-shell__brand">{t("app.brand")}</div>
<NavLink to="/files" className="app-shell__brand" aria-label={t("nav.files")}>
{t("app.brand")}
</NavLink>
<input
aria-label={t("field.search")}
placeholder={t("field.search")}
Expand Down
27 changes: 25 additions & 2 deletions packages/ui/src/i18n/t.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,37 @@ import en from "./locales/en-US.json";

export type Locale = "ko-KR" | "en-US";

const LOCALE_KEY = "ui.appearance.locale";

const dictionaries = {
"ko-KR": ko,
"en-US": en,
} as const;

export type I18nKey = keyof typeof dictionaries["ko-KR"];

export const t = (key: I18nKey, locale: Locale = "ko-KR"): string => {
const dict = dictionaries[locale] ?? dictionaries["ko-KR"];
let currentLocale: Locale = "ko-KR";

if (typeof window !== "undefined") {
const saved = window.localStorage.getItem(LOCALE_KEY);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Guard locale bootstrap from storage access failures

Reading window.localStorage at module initialization can throw a SecurityError in environments where storage is blocked (for example strict privacy settings or sandboxed contexts), and this code runs as soon as t.ts is imported by the app shell. That turns a recoverable preference read into an app-boot crash for all routes, so this read should be wrapped in a safe fallback path instead of executing unguarded at import time.

Useful? React with 👍 / 👎.

if (saved === "ko-KR" || saved === "en-US") {
currentLocale = saved;
}
}

export function getLocale(): Locale {
return currentLocale;
}

export function setLocale(locale: Locale) {
currentLocale = locale;
if (typeof window !== "undefined") {
window.localStorage.setItem(LOCALE_KEY, locale);
}
}

export const t = (key: I18nKey, locale?: Locale): string => {
const effectiveLocale = locale ?? currentLocale;
const dict = dictionaries[effectiveLocale] ?? dictionaries["ko-KR"];
return dict[key] ?? key;
};
Loading