Merge pull request 'refactor: enhance language handling and improve translation integration across components + make languages public for normal users' (#31) from refactor/reverseChargeTemplate into dev
Reviewed-on: #31
This commit is contained in:
commit
3f420c520f
@ -13,14 +13,14 @@ type Props = {
|
||||
};
|
||||
|
||||
export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdit }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { t, languages } = useTranslation();
|
||||
const [name, setName] = useState('');
|
||||
const [htmlCode, setHtmlCode] = useState('');
|
||||
const [isPreview, setIsPreview] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [statusMsg, setStatusMsg] = useState<string | null>(null);
|
||||
|
||||
const [lang, setLang] = useState<'en' | 'de'>('en');
|
||||
const [lang, setLang] = useState('en');
|
||||
const [type, setType] = useState<'contract' | 'invoice' | 'other'>('contract');
|
||||
const [contractType, setContractType] = useState<'contract' | 'gdpr' | 'abo'>('contract');
|
||||
const [userType, setUserType] = useState<'personal' | 'company' | 'both'>('personal');
|
||||
@ -35,6 +35,37 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi
|
||||
|
||||
const { uploadTemplate, updateTemplateState, getTemplate, reviseTemplate } = useContractManagement();
|
||||
|
||||
const availableLanguages = React.useMemo(() => {
|
||||
const byCode = new Map<string, { code: string; name: string }>();
|
||||
[
|
||||
{ code: 'en', name: 'English' },
|
||||
{ code: 'de', name: 'Deutsch' },
|
||||
...languages,
|
||||
].forEach((language) => {
|
||||
const code = String(language?.code || '').trim().toLowerCase();
|
||||
if (!code || byCode.has(code)) return;
|
||||
|
||||
byCode.set(code, {
|
||||
code,
|
||||
name: String(language?.name || code.toUpperCase()).trim() || code.toUpperCase(),
|
||||
});
|
||||
});
|
||||
|
||||
const priority = ['en', 'de'];
|
||||
return Array.from(byCode.values()).sort((left, right) => {
|
||||
const leftIndex = priority.indexOf(left.code);
|
||||
const rightIndex = priority.indexOf(right.code);
|
||||
|
||||
if (leftIndex !== -1 || rightIndex !== -1) {
|
||||
if (leftIndex === -1) return 1;
|
||||
if (rightIndex === -1) return -1;
|
||||
if (leftIndex !== rightIndex) return leftIndex - rightIndex;
|
||||
}
|
||||
|
||||
return left.name.localeCompare(right.name);
|
||||
});
|
||||
}, [languages]);
|
||||
|
||||
const resetEditorFields = () => {
|
||||
setName('');
|
||||
setHtmlCode('');
|
||||
@ -44,6 +75,12 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi
|
||||
setEditingMeta(null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!availableLanguages.length) return;
|
||||
if (availableLanguages.some((entry) => entry.code === lang)) return;
|
||||
setLang(availableLanguages[0].code);
|
||||
}, [availableLanguages, lang]);
|
||||
|
||||
// Load template into editor when editing
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
@ -58,7 +95,7 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi
|
||||
setName(tpl.name || '');
|
||||
setHtmlCode(tpl.html || '');
|
||||
setDescription(((tpl as any)?.description as string) || ''); // FIX: DocumentTemplate may not declare `description`
|
||||
setLang((tpl.lang as any) || 'en');
|
||||
setLang(String((tpl.lang as any) || 'en'));
|
||||
setType(((tpl.type as any) || 'contract') as 'contract' | 'invoice' | 'other');
|
||||
setContractType(((tpl.contract_type as any) || 'contract') as 'contract' | 'gdpr' | 'abo');
|
||||
setUserType(((tpl.user_type as any) || 'both') as 'personal' | 'company' | 'both');
|
||||
@ -85,7 +122,7 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi
|
||||
if (hasDoc) return src;
|
||||
// Minimal A4 skeleton so snippets render and print correctly
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="${lang}">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
@ -356,12 +393,13 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi
|
||||
</select>
|
||||
<select
|
||||
value={lang}
|
||||
onChange={(e) => setLang(e.target.value as 'en' | 'de')}
|
||||
onChange={(e) => setLang(e.target.value)}
|
||||
required
|
||||
className="w-full sm:w-32 rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm text-gray-900 outline-none focus:ring-2 focus:ring-indigo-500/50 shadow"
|
||||
>
|
||||
<option value="en">English (en)</option>
|
||||
<option value="de">Deutsch (de)</option>
|
||||
{availableLanguages.map((language) => (
|
||||
<option key={language.code} value={language.code}>{language.name} ({language.code})</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
@ -388,7 +426,7 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi
|
||||
onChange={(e) => setHtmlCode(e.target.value)}
|
||||
placeholder="Paste your full HTML (or snippet) here…"
|
||||
required
|
||||
className="min-h-[320px] w-full rounded-lg border border-gray-300 bg-white px-4 py-3 text-sm text-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 font-mono shadow"
|
||||
className="min-h-80 w-full rounded-lg border border-gray-300 bg-white px-4 py-3 text-sm text-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 font-mono shadow"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -13,6 +13,11 @@ type Props = {
|
||||
onEdit?: (id: string) => void;
|
||||
};
|
||||
|
||||
type LanguageEntry = {
|
||||
code: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
type ContractTemplate = {
|
||||
id: string;
|
||||
name: string;
|
||||
@ -160,7 +165,7 @@ const CONTRACT_TYPE_META: Record<ContractTypeKey, ContractTypeMeta> = {
|
||||
},
|
||||
};
|
||||
|
||||
const LANGUAGE_ORDER = ['de', 'en', 'other'];
|
||||
const LANGUAGE_ORDER = ['de', 'en'];
|
||||
|
||||
function normalizeFamily(type?: string | null): TemplateFamilyKey {
|
||||
if (type === 'contract') return 'contract';
|
||||
@ -185,7 +190,20 @@ function normalizeInvoiceTaxMode(taxMode?: string | null): InvoiceTaxModeKey {
|
||||
|
||||
function normalizeLanguage(lang?: string | null) {
|
||||
const value = (lang || '').toLowerCase();
|
||||
return value === 'de' || value === 'en' ? value : 'other';
|
||||
return value || 'other';
|
||||
}
|
||||
|
||||
function compareLanguageKeys(left: string, right: string) {
|
||||
const leftIndex = LANGUAGE_ORDER.indexOf(left);
|
||||
const rightIndex = LANGUAGE_ORDER.indexOf(right);
|
||||
|
||||
if (leftIndex !== -1 || rightIndex !== -1) {
|
||||
if (leftIndex === -1) return 1;
|
||||
if (rightIndex === -1) return -1;
|
||||
if (leftIndex !== rightIndex) return leftIndex - rightIndex;
|
||||
}
|
||||
|
||||
return left.localeCompare(right);
|
||||
}
|
||||
|
||||
function normalizeName(name?: string | null) {
|
||||
@ -203,26 +221,26 @@ function formatUserType(userType?: string | null) {
|
||||
}
|
||||
}
|
||||
|
||||
function formatLanguage(lang?: string | null) {
|
||||
switch (normalizeLanguage(lang)) {
|
||||
function formatLanguage(lang?: string | null, languages: LanguageEntry[] = []) {
|
||||
const normalized = normalizeLanguage(lang);
|
||||
const languageLabel = languages.find((entry) => normalizeLanguage(entry.code) === normalized)?.name;
|
||||
if (languageLabel) return languageLabel;
|
||||
|
||||
switch (normalized) {
|
||||
case 'de':
|
||||
return 'German';
|
||||
case 'en':
|
||||
return 'English';
|
||||
default:
|
||||
case 'other':
|
||||
return 'Other language';
|
||||
default:
|
||||
return normalized.toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
function formatLanguageCode(lang?: string | null) {
|
||||
switch (normalizeLanguage(lang)) {
|
||||
case 'de':
|
||||
return 'DE';
|
||||
case 'en':
|
||||
return 'EN';
|
||||
default:
|
||||
return 'OT';
|
||||
}
|
||||
const normalized = normalizeLanguage(lang);
|
||||
return normalized === 'other' ? 'OT' : normalized.toUpperCase();
|
||||
}
|
||||
|
||||
function formatInvoiceTaxMode(taxMode?: string | null) {
|
||||
@ -296,19 +314,19 @@ function buildTrackKey(template: ContractTemplate) {
|
||||
].join(':');
|
||||
}
|
||||
|
||||
function buildTrack(templates: ContractTemplate[]): TemplateTrack {
|
||||
function buildTrack(templates: ContractTemplate[], languages: LanguageEntry[] = []): TemplateTrack {
|
||||
const sortedTemplates = [...templates].sort(compareTemplates);
|
||||
const lead = sortedTemplates.find((template) => template.status === 'published') || sortedTemplates[0];
|
||||
const family = normalizeFamily(lead?.type);
|
||||
const names = Array.from(new Set(sortedTemplates.map((template) => template.name.trim()).filter(Boolean)));
|
||||
const languageKeys = Array.from(new Set(sortedTemplates.map((template) => normalizeLanguage(template.lang)))).sort(
|
||||
(left, right) => LANGUAGE_ORDER.indexOf(left) - LANGUAGE_ORDER.indexOf(right)
|
||||
compareLanguageKeys
|
||||
);
|
||||
const languageColumns = languageKeys.map((langKey) => {
|
||||
const templatesInLanguage = sortedTemplates.filter((template) => normalizeLanguage(template.lang) === langKey);
|
||||
return {
|
||||
key: langKey,
|
||||
label: formatLanguage(langKey),
|
||||
label: formatLanguage(langKey, languages),
|
||||
templates: templatesInLanguage,
|
||||
activeTemplate: templatesInLanguage.find((template) => template.status === 'published'),
|
||||
};
|
||||
@ -512,7 +530,7 @@ function TemplateVersionHistoryModal({
|
||||
}
|
||||
|
||||
export default function ContractTemplateList({ refreshKey = 0, onEdit }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { t, languages } = useTranslation();
|
||||
const [items, setItems] = useState<ContractTemplate[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [q, setQ] = useState('');
|
||||
@ -547,14 +565,14 @@ export default function ContractTemplateList({ refreshKey = 0, onEdit }: Props)
|
||||
formatContractType(item.contract_type),
|
||||
formatUserType(item.user_type),
|
||||
formatInvoiceTaxMode(item.tax_mode),
|
||||
formatLanguage(item.lang),
|
||||
formatLanguage(item.lang, languages),
|
||||
]
|
||||
.join(' ')
|
||||
.toLowerCase();
|
||||
|
||||
return haystack.includes(term);
|
||||
});
|
||||
}, [items, q]);
|
||||
}, [items, languages, q]);
|
||||
|
||||
const familyGroups = useMemo<TemplateFamilyGroup[]>(() => {
|
||||
const grouped = new Map<TemplateFamilyKey, Map<string, ContractTemplate[]>>();
|
||||
@ -580,7 +598,7 @@ export default function ContractTemplateList({ refreshKey = 0, onEdit }: Props)
|
||||
return FAMILY_ORDER.map((family) => {
|
||||
const trackMap = grouped.get(family) || new Map<string, ContractTemplate[]>();
|
||||
const tracks = Array.from(trackMap.values())
|
||||
.map((templates) => buildTrack(templates))
|
||||
.map((templates) => buildTrack(templates, languages))
|
||||
.sort((left, right) => {
|
||||
const activityDelta = right.activeCount - left.activeCount;
|
||||
if (activityDelta !== 0) return activityDelta;
|
||||
@ -598,7 +616,7 @@ export default function ContractTemplateList({ refreshKey = 0, onEdit }: Props)
|
||||
activeTemplates: allTemplates.filter((template) => template.status === 'published').length,
|
||||
};
|
||||
}).filter((group) => group.totalTemplates > 0);
|
||||
}, [filtered]);
|
||||
}, [filtered, languages]);
|
||||
|
||||
const activeFamily = familyGroups.find((group) => group.key === selectedFamily) || familyGroups[0] || null;
|
||||
const activeContractSections = useMemo<ContractTypeSection[]>(() => {
|
||||
|
||||
@ -204,10 +204,7 @@ async function writeLanguageFile(
|
||||
await fs.writeFile(filePath, content, 'utf8');
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const access = await requireAdminSession(request);
|
||||
if (!access.ok) return access.response;
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const data = await readAllTranslations();
|
||||
return NextResponse.json({ ok: true, ...data });
|
||||
|
||||
97
src/app/api/user/settings/route.ts
Normal file
97
src/app/api/user/settings/route.ts
Normal file
@ -0,0 +1,97 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { fetchBackendAccessToken } from '../../_utils/backendAuth';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
function resolveBackendBaseUrl(): string | null {
|
||||
const value = process.env.NEXT_PUBLIC_API_BASE_URL?.trim();
|
||||
return value || null;
|
||||
}
|
||||
|
||||
function resolveUserSettingsBackendPath(): string {
|
||||
const configured = process.env.BACKEND_USER_SETTINGS_PATH?.trim();
|
||||
if (configured) return configured.startsWith('/') ? configured : `/${configured}`;
|
||||
return '/api/user/settings';
|
||||
}
|
||||
|
||||
function readIncomingBearerToken(request: Request): string | null {
|
||||
const authorization = request.headers.get('authorization') ?? request.headers.get('Authorization');
|
||||
if (!authorization || !authorization.startsWith('Bearer ')) return null;
|
||||
|
||||
const token = authorization.slice('Bearer '.length).trim();
|
||||
return token || null;
|
||||
}
|
||||
|
||||
async function proxyUserSettingsRequest(request: Request, method: 'GET' | 'PUT') {
|
||||
const incomingAccessToken = readIncomingBearerToken(request);
|
||||
const tokenResult = incomingAccessToken
|
||||
? { ok: true, status: 200, accessToken: incomingAccessToken, setCookies: [] }
|
||||
: await fetchBackendAccessToken(request);
|
||||
|
||||
if (!tokenResult.ok || !tokenResult.accessToken) {
|
||||
const denied = NextResponse.json(
|
||||
{ ok: false, message: tokenResult.message ?? 'Unable to obtain backend access token.' },
|
||||
{ status: tokenResult.status === 401 ? 401 : 403 }
|
||||
);
|
||||
|
||||
for (const setCookie of tokenResult.setCookies) {
|
||||
denied.headers.append('set-cookie', setCookie);
|
||||
}
|
||||
|
||||
return denied;
|
||||
}
|
||||
|
||||
const apiBase = resolveBackendBaseUrl();
|
||||
if (!apiBase) {
|
||||
return NextResponse.json({ ok: false, message: 'Missing NEXT_PUBLIC_API_BASE_URL.' }, { status: 500 });
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
cookie: request.headers.get('cookie') ?? '',
|
||||
Authorization: `Bearer ${tokenResult.accessToken}`,
|
||||
};
|
||||
|
||||
let body: string | undefined;
|
||||
if (method === 'PUT') {
|
||||
const payload = await request.json().catch(() => ({}));
|
||||
headers['Content-Type'] = 'application/json';
|
||||
body = JSON.stringify(payload ?? {});
|
||||
}
|
||||
|
||||
const backendResponse = await fetch(`${apiBase}${resolveUserSettingsBackendPath()}`, {
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
cache: 'no-store',
|
||||
}).catch(() => null);
|
||||
|
||||
if (!backendResponse) {
|
||||
const failed = NextResponse.json(
|
||||
{ ok: false, message: 'User settings backend is unreachable.' },
|
||||
{ status: 502 }
|
||||
);
|
||||
|
||||
for (const setCookie of tokenResult.setCookies) {
|
||||
failed.headers.append('set-cookie', setCookie);
|
||||
}
|
||||
|
||||
return failed;
|
||||
}
|
||||
|
||||
const payload = await backendResponse.json().catch(() => null);
|
||||
const out = NextResponse.json(payload ?? { ok: backendResponse.ok }, { status: backendResponse.status });
|
||||
|
||||
for (const setCookie of tokenResult.setCookies) {
|
||||
out.headers.append('set-cookie', setCookie);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
return proxyUserSettingsRequest(request, 'GET');
|
||||
}
|
||||
|
||||
export async function PUT(request: Request) {
|
||||
return proxyUserSettingsRequest(request, 'PUT');
|
||||
}
|
||||
@ -1,10 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react';
|
||||
import { createContext, useContext, useState, useEffect, useCallback, useRef, ReactNode } from 'react';
|
||||
import { DEFAULT_LANGUAGE } from './config';
|
||||
import { en } from './translations/en';
|
||||
import { de } from './translations/de';
|
||||
import { sl } from './translations/sl';
|
||||
import { flattenObject } from './dynamicTranslations';
|
||||
import { authFetch } from '../utils/authFetch';
|
||||
import useAuthStore from '../store/authStore';
|
||||
import { API_ENDPOINTS } from '../utils/api';
|
||||
|
||||
type LanguageEntry = {
|
||||
code: string;
|
||||
@ -19,6 +23,7 @@ type TranslationFilesPayload = {
|
||||
const builtInTranslations: Record<string, Record<string, unknown>> = {
|
||||
en: en as unknown as Record<string, unknown>,
|
||||
de: de as unknown as Record<string, unknown>,
|
||||
sl: sl as unknown as Record<string, unknown>,
|
||||
};
|
||||
|
||||
const APP_LANGUAGE_STORAGE_KEY = 'pp-selected-language';
|
||||
@ -26,6 +31,26 @@ const APP_LANGUAGE_STORAGE_KEY = 'pp-selected-language';
|
||||
// Flat map of English keys used as canonical key list and fallback
|
||||
const enFlat = flattenObject(en as unknown as Record<string, unknown>);
|
||||
|
||||
function normalizeLanguageValue(value: unknown): string | null {
|
||||
if (typeof value !== 'string') return null;
|
||||
const normalized = value.trim().toLowerCase();
|
||||
return normalized ? normalized : null;
|
||||
}
|
||||
|
||||
function extractPreferredLanguage(payload: unknown): string | null {
|
||||
if (!payload || typeof payload !== 'object') return null;
|
||||
|
||||
const settings = (payload as { settings?: Record<string, unknown> }).settings;
|
||||
if (!settings || typeof settings !== 'object') return null;
|
||||
|
||||
return normalizeLanguageValue(
|
||||
settings.preferred_language
|
||||
?? settings.preferredLanguage
|
||||
?? settings.language
|
||||
?? settings.lang,
|
||||
);
|
||||
}
|
||||
|
||||
function getNestedValue(root: Record<string, unknown>, key: string): string | null {
|
||||
const segments = key.split('.');
|
||||
let current: unknown = root;
|
||||
@ -53,16 +78,21 @@ interface I18nProviderProps {
|
||||
}
|
||||
|
||||
export function I18nProvider({ children }: I18nProviderProps) {
|
||||
const accessToken = useAuthStore((state) => state.accessToken);
|
||||
const [language, setLanguage] = useState<string>(DEFAULT_LANGUAGE);
|
||||
const [hasSyncedStoredLanguage, setHasSyncedStoredLanguage] = useState(false);
|
||||
const [hasResolvedPersistedLanguage, setHasResolvedPersistedLanguage] = useState(false);
|
||||
const lastPersistedLanguageRef = useRef<string | null>(null);
|
||||
const [translationFiles, setTranslationFiles] = useState<TranslationFilesPayload>({
|
||||
languages: [
|
||||
{ code: 'en', name: 'English' },
|
||||
{ code: 'de', name: 'Deutsch' },
|
||||
{ code: 'sl', name: 'Slovenian' },
|
||||
],
|
||||
translations: {
|
||||
en: enFlat,
|
||||
de: flattenObject(de as unknown as Record<string, unknown>),
|
||||
sl: flattenObject(sl as unknown as Record<string, unknown>),
|
||||
},
|
||||
});
|
||||
|
||||
@ -101,13 +131,17 @@ export function I18nProvider({ children }: I18nProviderProps) {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const stored = window.localStorage.getItem(APP_LANGUAGE_STORAGE_KEY);
|
||||
if (stored && stored.trim() && stored !== language) {
|
||||
setLanguage(stored);
|
||||
if (stored && stored.trim()) {
|
||||
setLanguage((currentLanguage) => currentLanguage === stored ? currentLanguage : stored);
|
||||
}
|
||||
|
||||
setHasSyncedStoredLanguage(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
lastPersistedLanguageRef.current = null;
|
||||
}, [accessToken]);
|
||||
|
||||
useEffect(() => {
|
||||
if (translationFiles.languages.length === 0) return;
|
||||
if (translationFiles.languages.some((entry) => entry.code === language)) return;
|
||||
@ -118,11 +152,82 @@ export function I18nProvider({ children }: I18nProviderProps) {
|
||||
setLanguage(fallback);
|
||||
}, [translationFiles.languages, language]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasSyncedStoredLanguage) return;
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
async function syncPersistedLanguage() {
|
||||
if (!accessToken) {
|
||||
setHasResolvedPersistedLanguage(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setHasResolvedPersistedLanguage(false);
|
||||
try {
|
||||
const response = await authFetch(API_ENDPOINTS.USER_SETTINGS, {
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
});
|
||||
const result = await response.json().catch(() => null);
|
||||
const preferredLanguage = response.ok ? extractPreferredLanguage(result) : null;
|
||||
|
||||
if (!cancelled && preferredLanguage) {
|
||||
lastPersistedLanguageRef.current = preferredLanguage;
|
||||
setLanguage((currentLanguage) => currentLanguage === preferredLanguage ? currentLanguage : preferredLanguage);
|
||||
}
|
||||
} catch {
|
||||
// Ignore preference sync failures and keep local state.
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setHasResolvedPersistedLanguage(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void syncPersistedLanguage();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [accessToken, hasSyncedStoredLanguage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined' || !hasSyncedStoredLanguage) return;
|
||||
|
||||
window.localStorage.setItem(APP_LANGUAGE_STORAGE_KEY, language);
|
||||
document.documentElement.lang = language;
|
||||
}, [hasSyncedStoredLanguage, language]);
|
||||
|
||||
if (!accessToken || !hasResolvedPersistedLanguage || lastPersistedLanguageRef.current === language) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
async function persistPreferredLanguage() {
|
||||
try {
|
||||
const response = await authFetch(API_ENDPOINTS.USER_SETTINGS, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ preferredLanguage: language }),
|
||||
});
|
||||
|
||||
if (!cancelled && response.ok) {
|
||||
lastPersistedLanguageRef.current = language;
|
||||
}
|
||||
} catch {
|
||||
// Ignore persistence failures; localStorage remains as the client fallback.
|
||||
}
|
||||
}
|
||||
|
||||
void persistPreferredLanguage();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [accessToken, hasResolvedPersistedLanguage, hasSyncedStoredLanguage, language]);
|
||||
|
||||
const t = useCallback((key: string): string => {
|
||||
// 1. Check translation loaded from translation files API.
|
||||
|
||||
@ -10,7 +10,7 @@ import { useToast } from '../../../components/toast/toastComponent'
|
||||
import { useTranslation } from '../../../i18n/useTranslation'
|
||||
|
||||
export default function CompanySignContractPage() {
|
||||
const { t } = useTranslation();
|
||||
const { t, language } = useTranslation();
|
||||
const router = useRouter()
|
||||
const user = useAuthStore(s => s.user) // NEW
|
||||
const isAuthReady = useAuthStore(s => (s as any).isAuthReady) // NEW
|
||||
@ -125,7 +125,8 @@ export default function CompanySignContractPage() {
|
||||
[contractType]: { ...prev[contractType], loading: true, error: null }
|
||||
}))
|
||||
try {
|
||||
const res = await fetch(`${API_BASE_URL}/api/contracts/preview/latest?contract_type=${contractType}`, {
|
||||
const qs = new URLSearchParams({ contract_type: contractType, lang: language }).toString()
|
||||
const res = await fetch(`${API_BASE_URL}/api/contracts/preview/latest?${qs}`, {
|
||||
method: 'GET',
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
credentials: 'include'
|
||||
@ -156,7 +157,7 @@ export default function CompanySignContractPage() {
|
||||
loadPreview('contract'),
|
||||
loadPreview('gdpr')
|
||||
]).finally(() => setPreviewLoading(false))
|
||||
}, [accessToken])
|
||||
}, [accessToken, language])
|
||||
|
||||
useEffect(() => {
|
||||
const doneLoading = !previewState.contract.loading && !previewState.gdpr.loading && !previewLoading
|
||||
@ -246,6 +247,7 @@ export default function CompanySignContractPage() {
|
||||
const contractData = {
|
||||
date,
|
||||
contractType: 'company',
|
||||
lang: language,
|
||||
}
|
||||
|
||||
// Create FormData for the existing backend endpoint
|
||||
|
||||
@ -10,7 +10,7 @@ import { useToast } from '../../../components/toast/toastComponent'
|
||||
import { useTranslation } from '../../../i18n/useTranslation'
|
||||
|
||||
export default function PersonalSignContractPage() {
|
||||
const { t } = useTranslation();
|
||||
const { t, language } = useTranslation();
|
||||
const router = useRouter()
|
||||
const user = useAuthStore(s => s.user) // NEW
|
||||
const isAuthReady = useAuthStore(s => (s as any).isAuthReady) // NEW
|
||||
@ -130,8 +130,8 @@ export default function PersonalSignContractPage() {
|
||||
[contractType]: { ...prev[contractType], loading: true, error: null }
|
||||
}))
|
||||
try {
|
||||
const qs = contractType ? `?contract_type=${contractType}` : ''
|
||||
const res = await fetch(`${API_BASE_URL}/api/contracts/preview/latest${qs}`, {
|
||||
const qs = new URLSearchParams({ contract_type: contractType, lang: language }).toString()
|
||||
const res = await fetch(`${API_BASE_URL}/api/contracts/preview/latest?${qs}`, {
|
||||
method: 'GET',
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
credentials: 'include'
|
||||
@ -162,7 +162,7 @@ export default function PersonalSignContractPage() {
|
||||
loadPreview('contract'),
|
||||
loadPreview('gdpr')
|
||||
]).finally(() => setPreviewLoading(false))
|
||||
}, [accessToken])
|
||||
}, [accessToken, language])
|
||||
|
||||
// NEW: hard block if step already done OR all steps done
|
||||
const [redirectTo, setRedirectTo] = useState<string | null>(null)
|
||||
@ -284,7 +284,8 @@ export default function PersonalSignContractPage() {
|
||||
try {
|
||||
const contractData = {
|
||||
date,
|
||||
contractType: 'personal'
|
||||
contractType: 'personal',
|
||||
lang: language,
|
||||
}
|
||||
|
||||
// Create FormData for the backend endpoint (no dummy PDF needed; server generates from templates)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user