Browse Source

feat(dashboard): Implement content language handling

Michael Bromley 5 months ago
parent
commit
b635d23e19

+ 1 - 1
packages/dashboard/src/app/app-providers.tsx

@@ -14,7 +14,7 @@ export function AppProviders({ children }: { children: React.ReactNode }) {
     return (
         <I18nProvider>
             <QueryClientProvider client={queryClient}>
-                <UserSettingsProvider>
+                <UserSettingsProvider queryClient={queryClient}>
                     <ThemeProvider defaultTheme="system">
                         <AuthProvider>
                             <ServerConfigProvider>

+ 168 - 57
packages/dashboard/src/lib/components/layout/channel-switcher.tsx

@@ -1,4 +1,4 @@
-import { ChevronsUpDown, Plus } from 'lucide-react';
+import { ChevronsUpDown, Languages, Plus } from 'lucide-react';
 
 import { ChannelCodeLabel } from '@/vdb/components/shared/channel-code-label.js';
 import {
@@ -7,80 +7,191 @@ import {
     DropdownMenuItem,
     DropdownMenuLabel,
     DropdownMenuSeparator,
+    DropdownMenuSub,
+    DropdownMenuSubContent,
+    DropdownMenuSubTrigger,
     DropdownMenuTrigger,
 } from '@/vdb/components/ui/dropdown-menu.js';
 import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from '@/vdb/components/ui/sidebar.js';
 import { useChannel } from '@/vdb/hooks/use-channel.js';
+import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
+import { useServerConfig } from '@/vdb/hooks/use-server-config.js';
+import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
 import { Trans } from '@/vdb/lib/trans.js';
 import { Link } from '@tanstack/react-router';
+import { useState } from 'react';
+import { ManageLanguagesDialog } from './manage-languages-dialog.js';
+
+/**
+ * Convert the channel code to initials.
+ * Splits by punctuation like '-' and '_' and takes the first letter of each part
+ * up to 3 parts.
+ *
+ * If no splits, takes the first 3 letters.
+ */
+function getChannelInitialsFromCode(code: string) {
+    const parts = code.split(/[-_]/);
+    if (parts.length > 1) {
+        return parts
+            .filter(part => part.length > 0)
+            .slice(0, 3)
+            .map(part => part[0])
+            .join('');
+    } else {
+        return code.slice(0, 3);
+    }
+}
 
 export function ChannelSwitcher() {
     const { isMobile } = useSidebar();
     const { channels, activeChannel, selectedChannel, setSelectedChannel } = useChannel();
+    const serverConfig = useServerConfig();
+    const { formatLanguageName } = useLocalFormat();
+    const {
+        settings: { contentLanguage },
+        setContentLanguage,
+    } = useUserSettings();
+    const [showManageLanguagesDialog, setShowManageLanguagesDialog] = useState(false);
 
     // Use the selected channel if available, otherwise fall back to the active channel
     const displayChannel = selectedChannel || activeChannel;
 
+    // Get available languages from server config
+    const availableLanguages = serverConfig?.availableLanguages || [];
+    const hasMultipleLanguages = availableLanguages.length > 1;
+
+    // Reorder channels to put the currently selected one first
+    const orderedChannels = displayChannel
+        ? [displayChannel, ...channels.filter(ch => ch.id !== displayChannel.id)]
+        : channels;
+
+    console.log(displayChannel);
+
     return (
-        <SidebarMenu>
-            <SidebarMenuItem>
-                <DropdownMenu>
-                    <DropdownMenuTrigger asChild>
-                        <SidebarMenuButton
-                            size="lg"
-                            className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
-                        >
-                            <div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
-                                <span className="truncate font-semibold text-xs">
-                                    {displayChannel?.defaultCurrencyCode}
-                                </span>
-                            </div>
-                            <div className="grid flex-1 text-left text-sm leading-tight">
-                                <span className="truncate font-semibold">
-                                    <ChannelCodeLabel code={displayChannel?.code} />
-                                </span>
-                                <span className="truncate text-xs">
-                                    Default Language: {displayChannel?.defaultLanguageCode?.toUpperCase()}
-                                </span>
-                            </div>
-                            <ChevronsUpDown className="ml-auto" />
-                        </SidebarMenuButton>
-                    </DropdownMenuTrigger>
-                    <DropdownMenuContent
-                        className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
-                        align="start"
-                        side={isMobile ? 'bottom' : 'right'}
-                        sideOffset={4}
-                    >
-                        <DropdownMenuLabel className="text-muted-foreground text-xs">
-                            <Trans>Channels</Trans>
-                        </DropdownMenuLabel>
-                        {channels.map((channel, index) => (
-                            <DropdownMenuItem
-                                key={channel.code}
-                                onClick={() => setSelectedChannel(channel.id)}
-                                className="gap-2 p-2"
+        <>
+            <SidebarMenu>
+                <SidebarMenuItem>
+                    <DropdownMenu>
+                        <DropdownMenuTrigger asChild>
+                            <SidebarMenuButton
+                                size="lg"
+                                className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
                             >
-                                <div className="flex size-8 items-center justify-center rounded border">
-                                    <span className="truncate font-semibold text-xs">
-                                        {channel.defaultCurrencyCode}
+                                <div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
+                                    <span className="truncate font-semibold text-xs uppercase">
+                                        {getChannelInitialsFromCode(displayChannel?.code || '')}
                                     </span>
                                 </div>
-                                <ChannelCodeLabel code={channel.code} />
-                            </DropdownMenuItem>
-                        ))}
-                        <DropdownMenuSeparator />
-                        <DropdownMenuItem className="gap-2 p-2 cursor-pointer" asChild>
-                            <Link to={'/channels/new'}>
-                                <div className="bg-background flex size-6 items-center justify-center rounded-md border">
-                                    <Plus className="size-4" />
+                                <div className="grid flex-1 text-left text-sm leading-tight">
+                                    <span className="truncate font-semibold">
+                                        <ChannelCodeLabel code={displayChannel?.code} />
+                                    </span>
+                                    <span className="truncate text-xs">
+                                        {hasMultipleLanguages ? (
+                                            <span className="cursor-pointer hover:text-foreground">
+                                                Language: {formatLanguageName(contentLanguage)}
+                                            </span>
+                                        ) : (
+                                            <span>Language: {formatLanguageName(contentLanguage)}</span>
+                                        )}
+                                    </span>
                                 </div>
-                                <div className="text-muted-foreground font-medium">Add channel</div>
-                            </Link>
-                        </DropdownMenuItem>
-                    </DropdownMenuContent>
-                </DropdownMenu>
-            </SidebarMenuItem>
-        </SidebarMenu>
+                                <ChevronsUpDown className="ml-auto" />
+                            </SidebarMenuButton>
+                        </DropdownMenuTrigger>
+                        <DropdownMenuContent
+                            className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
+                            align="start"
+                            side={isMobile ? 'bottom' : 'right'}
+                            sideOffset={4}
+                        >
+                            <DropdownMenuLabel className="text-muted-foreground text-xs">
+                                <Trans>Channels</Trans>
+                            </DropdownMenuLabel>
+                            {orderedChannels.map((channel, index) => (
+                                <div key={channel.code}>
+                                    <DropdownMenuItem
+                                        onClick={() => setSelectedChannel(channel.id)}
+                                        className="gap-2 p-2"
+                                    >
+                                        <div className="flex size-8 items-center justify-center rounded border">
+                                            <span className="truncate font-semibold text-xs uppercase">
+                                                {getChannelInitialsFromCode(channel.code)}
+                                            </span>
+                                        </div>
+                                        <ChannelCodeLabel code={channel.code} />
+                                        {channel.id === displayChannel?.id && (
+                                            <span className="ml-auto text-xs text-muted-foreground">
+                                                Current
+                                            </span>
+                                        )}
+                                    </DropdownMenuItem>
+                                    {/* Show language sub-menu for the current channel */}
+                                    {channel.id === displayChannel?.id && (
+                                        <DropdownMenuSub>
+                                            <DropdownMenuSubTrigger className="gap-2 p-2 pl-4">
+                                                <Languages className="w-4 h-4" />
+                                                <div className="flex gap-1 ml-2">
+                                                    <span className="text-muted-foreground">Content: </span>
+                                                    {formatLanguageName(contentLanguage)}
+                                                </div>
+                                            </DropdownMenuSubTrigger>
+                                            <DropdownMenuSubContent>
+                                                {channel.availableLanguageCodes?.map(languageCode => (
+                                                    <DropdownMenuItem
+                                                        key={`${channel.code}-${languageCode}`}
+                                                        onClick={() => setContentLanguage(languageCode)}
+                                                        className={`gap-2 p-2 ${contentLanguage === languageCode ? 'bg-accent' : ''}`}
+                                                    >
+                                                        <div className="flex w-6 h-5 items-center justify-center rounded border">
+                                                            <span className="truncate font-medium text-xs">
+                                                                {languageCode.toUpperCase()}
+                                                            </span>
+                                                        </div>
+                                                        <span>{formatLanguageName(languageCode)}</span>
+                                                        {contentLanguage === languageCode && (
+                                                            <span className="ml-auto text-xs text-muted-foreground">
+                                                                Active
+                                                            </span>
+                                                        )}
+                                                    </DropdownMenuItem>
+                                                ))}
+                                                <DropdownMenuSeparator />
+                                                <DropdownMenuItem
+                                                    onClick={() => setShowManageLanguagesDialog(true)}
+                                                    className="gap-2 p-2"
+                                                >
+                                                    <Languages className="w-4 h-4" />
+                                                    <span>
+                                                        <Trans>Manage Languages</Trans>
+                                                    </span>
+                                                </DropdownMenuItem>
+                                            </DropdownMenuSubContent>
+                                        </DropdownMenuSub>
+                                    )}
+                                    {/* Add separator after the current channel group */}
+                                    {channel.id === displayChannel?.id &&
+                                        index === 0 &&
+                                        orderedChannels.length > 1 && <DropdownMenuSeparator />}
+                                </div>
+                            ))}
+                            <DropdownMenuSeparator />
+                            <DropdownMenuItem className="gap-2 p-2 cursor-pointer" asChild>
+                                <Link to={'/channels/new'}>
+                                    <div className="bg-background flex size-6 items-center justify-center rounded-md border">
+                                        <Plus className="size-4" />
+                                    </div>
+                                    <div className="text-muted-foreground font-medium">Add channel</div>
+                                </Link>
+                            </DropdownMenuItem>
+                        </DropdownMenuContent>
+                    </DropdownMenu>
+                </SidebarMenuItem>
+            </SidebarMenu>
+            <ManageLanguagesDialog
+                open={showManageLanguagesDialog}
+                onClose={() => setShowManageLanguagesDialog(false)}
+            />
+        </>
     );
 }

+ 77 - 40
packages/dashboard/src/lib/components/layout/manage-languages-dialog.tsx

@@ -1,3 +1,4 @@
+import { ChannelCodeLabel } from '@/vdb/components/shared/channel-code-label.js';
 import { LanguageSelector } from '@/vdb/components/shared/language-selector.js';
 import { Button } from '@/vdb/components/ui/button.js';
 import {
@@ -16,7 +17,6 @@ import { graphql } from '@/vdb/graphql/graphql.js';
 import { useChannel } from '@/vdb/hooks/use-channel.js';
 import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
 import { usePermissions } from '@/vdb/hooks/use-permissions.js';
-import { ChannelCodeLabel } from '@/vdb/components/shared/channel-code-label.js';
 import { Trans } from '@/vdb/lib/trans.js';
 import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
 import { AlertCircle, Lock } from 'lucide-react';
@@ -69,7 +69,24 @@ const updateChannelDocument = graphql(`
 
 // All possible language codes for global settings - includes more than what might be globally enabled
 const ALL_LANGUAGE_CODES = [
-    'en', 'es', 'fr', 'de', 'it', 'pt', 'nl', 'pl', 'ru', 'ja', 'zh', 'ko', 'ar', 'hi', 'sv', 'da', 'no', 'fi'
+    'en',
+    'es',
+    'fr',
+    'de',
+    'it',
+    'pt',
+    'nl',
+    'pl',
+    'ru',
+    'ja',
+    'zh',
+    'ko',
+    'ar',
+    'hi',
+    'sv',
+    'da',
+    'no',
+    'fi',
 ];
 
 interface ManageLanguagesDialogProps {
@@ -82,12 +99,13 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
     const { activeChannel, selectedChannel } = useChannel();
     const { hasPermissions } = usePermissions();
     const queryClient = useQueryClient();
-    
+
     const displayChannel = selectedChannel || activeChannel;
-    
+
     // Permission checks
     const canReadGlobalSettings = hasPermissions(['ReadSettings']) || hasPermissions(['ReadGlobalSettings']);
-    const canUpdateGlobalSettings = hasPermissions(['UpdateSettings']) || hasPermissions(['UpdateGlobalSettings']);
+    const canUpdateGlobalSettings =
+        hasPermissions(['UpdateSettings']) || hasPermissions(['UpdateGlobalSettings']);
     const canReadChannel = hasPermissions(['ReadChannel']);
     const canUpdateChannel = hasPermissions(['UpdateChannel']);
 
@@ -97,10 +115,10 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
     const [channelDefaultLanguage, setChannelDefaultLanguage] = useState<string>('');
 
     // Queries
-    const { 
-        data: globalSettingsData, 
+    const {
+        data: globalSettingsData,
         isLoading: globalSettingsLoading,
-        error: globalSettingsError 
+        error: globalSettingsError,
     } = useQuery({
         queryKey: ['globalSettings', 'languages'],
         queryFn: () => api.query(globalSettingsLanguagesDocument),
@@ -121,8 +139,11 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
     });
 
     const updateChannelMutation = useMutation({
-        mutationFn: (input: { id: string; availableLanguageCodes?: string[]; defaultLanguageCode?: string }) =>
-            api.mutate(updateChannelDocument, { input }),
+        mutationFn: (input: {
+            id: string;
+            availableLanguageCodes?: string[];
+            defaultLanguageCode?: string;
+        }) => api.mutate(updateChannelDocument, { input }),
         onSuccess: () => {
             queryClient.invalidateQueries({ queryKey: ['channels'] });
             toast.success('Channel language settings updated successfully');
@@ -145,11 +166,11 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
 
     const handleGlobalLanguagesChange = (newLanguages: string[]) => {
         setGlobalLanguages(newLanguages);
-        
+
         // Remove channel languages that are no longer in global languages
         const updatedChannelLanguages = channelLanguages.filter(lang => newLanguages.includes(lang));
         setChannelLanguages(updatedChannelLanguages);
-        
+
         // If the default language is no longer available, reset it
         if (!newLanguages.includes(channelDefaultLanguage)) {
             setChannelDefaultLanguage(updatedChannelLanguages[0] || '');
@@ -158,7 +179,7 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
 
     const handleChannelLanguagesChange = (newLanguages: string[]) => {
         setChannelLanguages(newLanguages);
-        
+
         // If the default language is no longer available, reset it
         if (!newLanguages.includes(channelDefaultLanguage)) {
             setChannelDefaultLanguage(newLanguages[0] || '');
@@ -172,7 +193,9 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
         if (canUpdateGlobalSettings && globalSettingsData) {
             const currentGlobalLanguages = globalSettingsData.globalSettings.availableLanguages || [];
             if (JSON.stringify(currentGlobalLanguages.sort()) !== JSON.stringify(globalLanguages.sort())) {
-                promises.push(updateGlobalSettingsMutation.mutateAsync({ availableLanguages: globalLanguages }));
+                promises.push(
+                    updateGlobalSettingsMutation.mutateAsync({ availableLanguages: globalLanguages }),
+                );
             }
         }
 
@@ -180,16 +203,19 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
         if (canUpdateChannel && displayChannel) {
             const currentChannelLanguages = displayChannel.availableLanguageCodes || [];
             const currentChannelDefault = displayChannel.defaultLanguageCode || '';
-            
-            const languagesChanged = JSON.stringify(currentChannelLanguages.sort()) !== JSON.stringify(channelLanguages.sort());
+
+            const languagesChanged =
+                JSON.stringify(currentChannelLanguages.sort()) !== JSON.stringify(channelLanguages.sort());
             const defaultChanged = currentChannelDefault !== channelDefaultLanguage;
-            
+
             if (languagesChanged || defaultChanged) {
-                promises.push(updateChannelMutation.mutateAsync({
-                    id: displayChannel.id,
-                    availableLanguageCodes: channelLanguages,
-                    defaultLanguageCode: channelDefaultLanguage,
-                }));
+                promises.push(
+                    updateChannelMutation.mutateAsync({
+                        id: displayChannel.id,
+                        availableLanguageCodes: channelLanguages,
+                        defaultLanguageCode: channelDefaultLanguage,
+                    }),
+                );
             }
         }
 
@@ -208,17 +234,17 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
                 return true;
             }
         }
-        
+
         if (displayChannel && canUpdateChannel) {
             const currentChannelLangs = displayChannel.availableLanguageCodes || [];
             const currentChannelDefault = displayChannel.defaultLanguageCode || '';
-            
+
             return (
                 JSON.stringify(currentChannelLangs.sort()) !== JSON.stringify(channelLanguages.sort()) ||
                 currentChannelDefault !== channelDefaultLanguage
             );
         }
-        
+
         return false;
     };
 
@@ -228,7 +254,9 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
         <Dialog open={open} onOpenChange={onClose}>
             <DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
                 <DialogHeader>
-                    <DialogTitle><Trans>Manage Languages</Trans></DialogTitle>
+                    <DialogTitle>
+                        <Trans>Manage Languages</Trans>
+                    </DialogTitle>
                     <DialogDescription>
                         <Trans>Configure available languages for your store and channels</Trans>
                     </DialogDescription>
@@ -238,10 +266,12 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
                     {/* Global Settings Section */}
                     <div>
                         <div className="flex items-center gap-2 mb-3">
-                            <h3 className="text-lg font-semibold"><Trans>Global Languages</Trans></h3>
+                            <h3 className="font-semibold">
+                                <Trans>Global Languages</Trans>
+                            </h3>
                             {!canReadGlobalSettings && <Lock className="h-4 w-4 text-muted-foreground" />}
                         </div>
-                        
+
                         {!canReadGlobalSettings ? (
                             <div className="flex items-center gap-2 p-3 bg-muted rounded-md">
                                 <AlertCircle className="h-4 w-4 text-muted-foreground" />
@@ -262,10 +292,14 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
                             </div>
                         ) : (
                             <div className="space-y-2">
-                                <Label className="text-sm font-medium">
+                                <Label>
                                     <Trans>Select Available Languages</Trans>
                                 </Label>
-                                <div className={!canUpdateGlobalSettings ? 'pointer-events-none opacity-50' : ''}>
+                                <div
+                                    className={
+                                        !canUpdateGlobalSettings ? 'pointer-events-none opacity-50' : ''
+                                    }
+                                >
                                     <LanguageSelector
                                         value={globalLanguages}
                                         onChange={handleGlobalLanguagesChange}
@@ -285,8 +319,9 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
                     {/* Channel Settings Section */}
                     <div>
                         <div className="flex items-center gap-2 mb-3">
-                            <h3 className="text-lg font-semibold">
-                                <Trans>Channel Languages</Trans> - <ChannelCodeLabel code={displayChannel?.code} />
+                            <h3 className="font-semibold">
+                                <Trans>Channel Languages</Trans> -{' '}
+                                <ChannelCodeLabel code={displayChannel?.code} />
                             </h3>
                             {!canReadChannel && <Lock className="h-4 w-4 text-muted-foreground" />}
                         </div>
@@ -304,7 +339,9 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
                                     <Label className="text-sm font-medium">
                                         <Trans>Available Languages</Trans>
                                     </Label>
-                                    <div className={!canUpdateChannel ? 'pointer-events-none opacity-50' : ''}>
+                                    <div
+                                        className={!canUpdateChannel ? 'pointer-events-none opacity-50' : ''}
+                                    >
                                         <LanguageSelector
                                             value={channelLanguages}
                                             onChange={handleChannelLanguagesChange}
@@ -318,7 +355,9 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
                                         </p>
                                     ) : (
                                         <p className="text-xs text-muted-foreground">
-                                            <Trans>Select from globally available languages for this channel</Trans>
+                                            <Trans>
+                                                Select from globally available languages for this channel
+                                            </Trans>
                                         </p>
                                     )}
                                 </div>
@@ -339,7 +378,8 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
                                             <SelectContent>
                                                 {channelLanguages.map(languageCode => (
                                                     <SelectItem key={languageCode} value={languageCode}>
-                                                        {formatLanguageName(languageCode)} ({languageCode.toUpperCase()})
+                                                        {formatLanguageName(languageCode)} (
+                                                        {languageCode.toUpperCase()})
                                                     </SelectItem>
                                                 ))}
                                             </SelectContent>
@@ -355,14 +395,11 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
                     <Button variant="outline" onClick={onClose} disabled={isLoading}>
                         <Trans>Cancel</Trans>
                     </Button>
-                    <Button 
-                        onClick={handleSave} 
-                        disabled={!hasChanges() || isLoading}
-                    >
+                    <Button onClick={handleSave} disabled={!hasChanges() || isLoading}>
                         {isLoading ? <Trans>Saving...</Trans> : <Trans>Save Changes</Trans>}
                     </Button>
                 </DialogFooter>
             </DialogContent>
         </Dialog>
     );
-}
+}

+ 16 - 2
packages/dashboard/src/lib/components/shared/translatable-form-field.tsx

@@ -1,6 +1,8 @@
 import { OverriddenFormComponent } from '@/vdb/framework/form-engine/overridden-form-component.js';
 import { LocationWrapper } from '@/vdb/framework/layout-engine/location-wrapper.js';
+import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
 import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
+import { Trans } from '@/vdb/lib/trans.js';
 import { Controller, ControllerProps, FieldPath, FieldValues } from 'react-hook-form';
 import { FormControl, FormDescription, FormItem, FormLabel, FormMessage } from '../ui/form.js';
 import { FormFieldWrapper } from './form-field-wrapper.js';
@@ -13,6 +15,7 @@ export type TranslatableFormFieldProps<TFieldValues extends TranslatableEntity |
     ControllerProps<TFieldValues>,
     'name'
 > & {
+    label?: React.ReactNode;
     name: TFieldValues extends TranslatableEntity
         ? keyof Omit<NonNullable<TFieldValues['translations']>[number], 'languageCode'>
         : TFieldValues extends TranslatableEntity[]
@@ -24,16 +27,26 @@ export const TranslatableFormField = <
     TFieldValues extends TranslatableEntity | TranslatableEntity[] = TranslatableEntity,
 >({
     name,
+    label,
     ...props
 }: TranslatableFormFieldProps<TFieldValues>) => {
+    const { formatLanguageName } = useLocalFormat();
     const { contentLanguage } = useUserSettings().settings;
     const formValues = props.control?._formValues;
     const translations = Array.isArray(formValues) ? formValues?.[0].translations : formValues?.translations;
-    const index = translations?.findIndex(
+    const existingIndex = translations?.findIndex(
         (translation: any) => translation?.languageCode === contentLanguage,
     );
+    const index = existingIndex === -1 ? translations?.length : existingIndex;
     if (index === undefined || index === -1) {
-        return null;
+        return (
+            <FormItem>
+                {label && <FormLabel>{label}</FormLabel>}
+                <div className="text-sm text-muted-foreground">
+                    <Trans>No translation found for {formatLanguageName(contentLanguage)}</Trans>
+                </div>
+            </FormItem>
+        );
     }
     const translationName = `translations.${index}.${String(name)}` as FieldPath<TFieldValues>;
     return <Controller {...props} name={translationName} key={translationName} />;
@@ -57,6 +70,7 @@ export const TranslatableFormFieldWrapper = <
     return (
         <LocationWrapper identifier={name as string}>
             <TranslatableFormField
+                label={label}
                 control={props.control}
                 name={name}
                 render={renderArgs => (

+ 20 - 1
packages/dashboard/src/lib/graphql/api.ts

@@ -19,7 +19,26 @@ const awesomeClient = new AwesomeGraphQLClient({
             headers.set('vendure-token', channelToken);
         }
 
-        return fetch(url, {
+        // Get the content language from user settings and add as query parameter
+        let finalUrl = url;
+        try {
+            const userSettings = localStorage.getItem('vendure-user-settings');
+            if (userSettings) {
+                const settings = JSON.parse(userSettings);
+                const contentLanguage = settings.contentLanguage;
+
+                if (contentLanguage) {
+                    const urlObj = new URL(finalUrl);
+                    urlObj.searchParams.set('languageCode', contentLanguage);
+                    finalUrl = urlObj.toString();
+                }
+            }
+        } catch (error) {
+            // eslint-disable-next-line no-console
+            console.warn('Failed to read content language from user settings:', error);
+        }
+
+        return fetch(finalUrl, {
             ...options,
             headers,
             credentials: 'include',

+ 13 - 11
packages/dashboard/src/lib/providers/channel-provider.tsx

@@ -13,6 +13,7 @@ const channelFragment = graphql(`
         defaultLanguageCode
         defaultCurrencyCode
         pricesIncludeTax
+        availableLanguageCodes
     }
 `);
 
@@ -92,17 +93,18 @@ export function ChannelProvider({ children }: Readonly<{ children: React.ReactNo
         // If user has specific channels assigned (non-superadmin), use those
         if (userChannels && userChannels.length > 0) {
             // Map user channels to match the Channel type structure
-            return userChannels.map(ch => ({
-                id: ch.id,
-                code: ch.code,
-                token: ch.token,
-                defaultLanguageCode:
-                    channelsData?.channels.items.find(c => c.id === ch.id)?.defaultLanguageCode || 'en',
-                defaultCurrencyCode:
-                    channelsData?.channels.items.find(c => c.id === ch.id)?.defaultCurrencyCode || 'USD',
-                pricesIncludeTax:
-                    channelsData?.channels.items.find(c => c.id === ch.id)?.pricesIncludeTax || false,
-            }));
+            return userChannels.map(ch => {
+                const fullChannelData = channelsData?.channels.items.find(c => c.id === ch.id);
+                return {
+                    id: ch.id,
+                    code: ch.code,
+                    token: ch.token,
+                    defaultLanguageCode: fullChannelData?.defaultLanguageCode || 'en',
+                    defaultCurrencyCode: fullChannelData?.defaultCurrencyCode || 'USD',
+                    pricesIncludeTax: fullChannelData?.pricesIncludeTax || false,
+                    availableLanguageCodes: fullChannelData?.availableLanguageCodes || ['en'],
+                };
+            });
         }
         // Otherwise use all channels (superadmin)
         return channelsData?.channels.items || [];

+ 18 - 3
packages/dashboard/src/lib/providers/user-settings.tsx

@@ -1,6 +1,7 @@
-import React, { createContext, useState, useEffect } from 'react';
-import { Theme } from './theme-provider.js';
+import { QueryClient } from '@tanstack/react-query';
 import { ColumnFiltersState } from '@tanstack/react-table';
+import React, { createContext, useEffect, useRef, useState } from 'react';
+import { Theme } from './theme-provider.js';
 
 export interface TableSettings {
     columnVisibility?: Record<string, boolean>;
@@ -57,7 +58,12 @@ export const UserSettingsContext = createContext<UserSettingsContextType | undef
 
 const STORAGE_KEY = 'vendure-user-settings';
 
-export const UserSettingsProvider: React.FC<React.PropsWithChildren<{}>> = ({ children }) => {
+interface UserSettingsProviderProps {
+    queryClient?: QueryClient;
+    children: React.ReactNode;
+}
+
+export const UserSettingsProvider: React.FC<UserSettingsProviderProps> = ({ queryClient, children }) => {
     // Load settings from localStorage or use defaults
     const loadSettings = (): UserSettings => {
         try {
@@ -72,6 +78,7 @@ export const UserSettingsProvider: React.FC<React.PropsWithChildren<{}>> = ({ ch
     };
 
     const [settings, setSettings] = useState<UserSettings>(loadSettings);
+    const previousContentLanguage = useRef(settings.contentLanguage);
 
     // Save settings to localStorage whenever they change
     useEffect(() => {
@@ -82,6 +89,14 @@ export const UserSettingsProvider: React.FC<React.PropsWithChildren<{}>> = ({ ch
         }
     }, [settings]);
 
+    // Invalidate all queries when content language changes
+    useEffect(() => {
+        if (queryClient && settings.contentLanguage !== previousContentLanguage.current) {
+            queryClient.invalidateQueries();
+            previousContentLanguage.current = settings.contentLanguage;
+        }
+    }, [settings.contentLanguage, queryClient]);
+
     // Settings updaters
     const updateSetting = <K extends keyof UserSettings>(key: K, value: UserSettings[K]) => {
         setSettings(prev => ({ ...prev, [key]: value }));