Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
36 changes: 33 additions & 3 deletions ui/desktop/src/components/Layout/NavigationPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,20 @@ import { AppEvents } from '../../constants/events';
import { CondensedRenderer } from './CondensedRenderer';
import { ExpandedRenderer } from './ExpandedRenderer';
import { NavigationOverlay } from './navigation';
import { defineMessages, useIntl } from '../../i18n';
import type { SessionStatus, DragHandlers } from './navigation/types';

const i18n = defineMessages({
home: { id: 'navigationCustomization.itemHome', defaultMessage: 'Home' },
chat: { id: 'navigationCustomization.itemChat', defaultMessage: 'Chat' },
recipes: { id: 'navigationCustomization.itemRecipes', defaultMessage: 'Recipes' },
skills: { id: 'skillsView.skillsTitle', defaultMessage: 'Skills' },
apps: { id: 'navigationCustomization.itemApps', defaultMessage: 'Apps' },
scheduler: { id: 'navigationCustomization.itemScheduler', defaultMessage: 'Scheduler' },
extensions: { id: 'navigationCustomization.itemExtensions', defaultMessage: 'Extensions' },
settings: { id: 'navigationCustomization.itemSettings', defaultMessage: 'Settings' },
});

export const Navigation: React.FC<{ className?: string }> = ({ className }) => {
const {
isNavExpanded,
Expand All @@ -26,6 +38,21 @@ export const Navigation: React.FC<{ className?: string }> = ({ className }) => {

const location = useLocation();
const { extensionsList } = useConfig();
const intl = useIntl();

const translatedLabels = useMemo(
() => ({
home: intl.formatMessage(i18n.home),
chat: intl.formatMessage(i18n.chat),
recipes: intl.formatMessage(i18n.recipes),
skills: intl.formatMessage(i18n.skills),
apps: intl.formatMessage(i18n.apps),
scheduler: intl.formatMessage(i18n.scheduler),
extensions: intl.formatMessage(i18n.extensions),
settings: intl.formatMessage(i18n.settings),
}),
[intl]
);

const appsExtensionEnabled = !!extensionsList?.find((ext) => ext.name === 'apps')?.enabled;

Expand All @@ -37,8 +64,12 @@ export const Navigation: React.FC<{ className?: string }> = ({ className }) => {
.filter((item) => {
if (item.path === '/apps') return appsExtensionEnabled;
return true;
});
}, [preferences.itemOrder, preferences.enabledItems, appsExtensionEnabled]);
})
.map((item) => ({
...item,
label: translatedLabels[item.id as keyof typeof translatedLabels] ?? item.label,
}));
}, [preferences.itemOrder, preferences.enabledItems, appsExtensionEnabled, translatedLabels]);

const isActive = useCallback((path: string) => location.pathname === path, [location.pathname]);

Expand Down Expand Up @@ -202,7 +233,6 @@ export const Navigation: React.FC<{ className?: string }> = ({ className }) => {

if (isOverlayMode) {
if (effectiveNavigationStyle === 'expanded') {
// Expanded overlay uses its own AnimatePresence layout
return content;
}
return (
Expand Down
35 changes: 21 additions & 14 deletions ui/desktop/src/i18n/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
export { defineMessages, useIntl } from 'react-intl';

/** The set of locales that have translation catalogs. */
const SUPPORTED_LOCALES = new Set(['en']);
const SUPPORTED_LOCALES = new Set(['en', 'zh', 'zh-CN']);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Normalize underscore locale tags before matching locales

With zh/zh-CN now listed as supported, GOOSE_LOCALE=zh_CN becomes a practical input, but getLocale() still compares the raw tag and only derives a base via split('-'), so zh_CN misses both checks and falls back to English. Meanwhile isZhLocale() in main.ts already normalizes _ to -, so this produces a mixed-language app (Chinese native menu, English renderer). Normalize candidate tags in getLocale() before matching/canonicalizing to keep renderer and menu localization consistent.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks! Addressed in 1acf963.

I updated i18n locale resolution to normalize underscore tags (e.g. zh_CN -> zh-CN) before canonicalization/matching, so renderer locale selection stays consistent with native menu locale handling.

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 Keep zh locales unsupported until catalogs exist

Adding 'zh' and 'zh-CN' to SUPPORTED_LOCALES makes getLocale() return those values, so the renderer now calls loadMessages('zh' | 'zh-CN'); however the repo only ships src/i18n/messages/en.json (and i18n-compile only compiles files in that folder), so those imports fall back to {} and the app runs with Chinese locale formatting but English UI strings plus a warning on startup. This is a user-visible regression for anyone setting GOOSE_LOCALE=zh*; either commit the corresponding zh catalog(s) or defer advertising these locales as supported.

Useful? React with 👍 / 👎.


/**
* Detect the user's preferred locale.
Expand All @@ -36,23 +36,30 @@ export function getLocale(): { locale: string; messageLocale: string } {
candidates.push(navigator.language);
}

for (const tag of candidates) {
for (const candidate of candidates) {
const normalized = candidate.replace(/_/g, '-');

let canonical: string | undefined;
try {
[canonical] = Intl.getCanonicalLocales(normalized);
} catch {
canonical = undefined;
}

const resolved = canonical ?? normalized;

// Exact match first
if (SUPPORTED_LOCALES.has(tag)) return { locale: tag, messageLocale: tag };
if (SUPPORTED_LOCALES.has(resolved)) {
return { locale: resolved, messageLocale: resolved };
}

// Try base language (e.g. "pt-BR" → "pt") for the catalog, but keep the
// full regional tag for formatting so date/number output respects the region.
const base = tag.split('-')[0];
const base = resolved.split('-')[0];
if (SUPPORTED_LOCALES.has(base)) {
// Validate the full tag is a well-formed BCP 47 locale before using it
// for formatting. Invalid tags (e.g. "en-") would cause RangeError in
// Intl APIs, so fall back to the base language in that case.
let locale = base;
try {
[locale] = Intl.getCanonicalLocales(tag);
} catch {
// tag is not valid BCP 47 — use the base language instead
}
return { locale, messageLocale: base };
// If canonicalization fails, return the base locale so downstream Intl APIs
// never receive malformed tags like "en-".
return { locale: canonical ?? base, messageLocale: base };
}
}

Expand Down
Loading
Loading