refactor: enhance i18n handling by implementing state management for active language and optimizing resource loading

This commit is contained in:
Firman Ramdhani
2026-05-28 13:52:34 +07:00
parent d0ebceb1bb
commit dff05748f5
2 changed files with 112 additions and 55 deletions
@@ -16,26 +16,50 @@ const sectionStyle = {
background: '#0f172a',
};
const btnStyle = (color: string) => ({
const btnStyle = (color: string, isActive: boolean = false) => ({
padding: '8px 16px',
fontSize: 14,
fontWeight: 600 as const,
fontWeight: isActive ? 700 : 600,
cursor: 'pointer' as const,
background: color,
color: '#fff',
border: 'none',
border: isActive ? '2px solid #fff' : '2px solid transparent',
borderRadius: 6,
marginRight: 8,
});
// ─── Module-Level Flag ──────────────────────────────────────────
// Bendera penanda statis agar kamus hanya dimuat satu kali
let isBookingDictLoaded = false;
// ─── Component ──────────────────────────────────────────────────
export default function I18nSample() {
// 1. Eksekusi SINKRONUS tepat sebelum render pertama
if (!isBookingDictLoaded) {
i18n.addResourceBundle('id', 'booking', bookingId, true, false);
i18n.addResourceBundle('en', 'booking', bookingEn, true, false);
isBookingDictLoaded = true;
}
// 2. Sekarang useTranslation dijamin mendapat kamus yang sudah terisi penuh
const { t } = useTranslation(['common', 'booking']);
// State untuk melacak bahasa aktif secara real-time
const [activeLang, setActiveLang] = useState(i18n.language);
const [syncStatus, setSyncStatus] = useState<string>('');
const [activeTenant, setActiveTenant] = useState<string>('default');
const [isFetchingConfig, setIsFetchingConfig] = useState(false);
// Dengarkan perubahan bahasa dari engine
useEffect(() => {
const handleLangChange = (lng: string) => setActiveLang(lng);
i18n.on('languageChanged', handleLangChange);
return () => {
i18n.off('languageChanged', handleLangChange);
};
}, []);
// ─── Admin Panel State ──────────────────────────────────────────
const [adminModuleName, setAdminModuleName] = useState('PENGELUARAN');
const [adminHeaderTitle, setAdminHeaderTitle] = useState('Daftar Pengeluaran');
@@ -80,7 +104,6 @@ export default function I18nSample() {
}
return data;
} else if (companyId === 'company-b') {
// Hardcoded fallback for B
return {
namespace: 'booking',
overrides: {
@@ -92,13 +115,6 @@ export default function I18nSample() {
throw new Error('Unknown company');
};
// 1. Lazy-load the 'booking' namespace when the module mounts
useEffect(() => {
// Check if it's already loaded to prevent duplicate work, but for safety:
i18n.addResourceBundle('id', 'booking', bookingId, true, false);
i18n.addResourceBundle('en', 'booking', bookingEn, true, false);
}, []);
// ─── Section A: Language Switcher ──────────────────────────────
const handleLanguageChange = async (newLng: string, shouldFail: boolean = false) => {
@@ -106,7 +122,6 @@ export default function I18nSample() {
try {
await changeLanguage(newLng, async (lng, _prevLng) => {
// Mock API Call
await new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldFail) {
@@ -117,7 +132,6 @@ export default function I18nSample() {
}, 1000);
});
// If success
setSyncStatus(`✅ Successfully synced language '${lng}' to backend.`);
});
} catch (error) {
@@ -132,11 +146,7 @@ export default function I18nSample() {
setActiveTenant(companyId);
try {
// 1. App successfully authenticates and fetches config
const config = await mockFetchTenantConfig(companyId);
// 2. Inject the deep-merge payload returned from the server
// In a real app, you might apply this to the current active language or all languages.
applyTenantOverrides(config.namespace, config.overrides, 'id');
applyTenantOverrides(config.namespace, config.overrides, 'en');
} catch (err) {
@@ -147,7 +157,6 @@ export default function I18nSample() {
};
const resetTenant = () => {
// To reset, we just reload the original bundles
i18n.addResourceBundle('id', 'booking', bookingId, true, true);
i18n.addResourceBundle('en', 'booking', bookingEn, true, true);
setActiveTenant('default');
@@ -157,7 +166,7 @@ export default function I18nSample() {
<div style={{ fontFamily: 'sans-serif', color: '#f8fafc' }}>
<h2 style={{ fontSize: 24, fontWeight: 'bold' }}>🌐 Enterprise i18n Demo</h2>
<p style={{ color: '#94a3b8' }}>
Current Active Language: <strong style={{ color: '#38bdf8' }}>{i18n.language}</strong>
Current Active Language: <strong style={{ color: '#38bdf8' }}>{activeLang}</strong>
</p>
{/* ─── Admin Panel ──────────────────────────────────────────── */}
@@ -222,10 +231,16 @@ export default function I18nSample() {
</p>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
<button onClick={() => handleLanguageChange('id')} style={btnStyle('#0284c7')}>
<button
onClick={() => handleLanguageChange('id')}
style={btnStyle(activeLang === 'id' ? '#1d4ed8' : '#0ea5e9', activeLang === 'id')}
>
ID (Lokal & Sync)
</button>
<button onClick={() => handleLanguageChange('en')} style={btnStyle('#0284c7')}>
<button
onClick={() => handleLanguageChange('en')}
style={btnStyle(activeLang === 'en' ? '#1d4ed8' : '#0ea5e9', activeLang === 'en')}
>
EN (Lokal & Sync)
</button>
<button onClick={() => handleLanguageChange('en', true)} style={btnStyle('#dc2626')}>
@@ -246,6 +261,19 @@ export default function I18nSample() {
{syncStatus}
</div>
)}
{/* UI Result untuk Section A */}
<div style={{ background: '#1e293b', padding: 16, borderRadius: 8, marginTop: 16 }}>
<h4 style={{ color: '#cbd5e1', marginBottom: 12 }}>UI Result (Live Dictionary):</h4>
<p style={{ margin: '4px 0', display: 'flex', alignItems: 'center' }}>
<code style={{ color: '#94a3b8', width: 180, display: 'inline-block' }}>common:save</code>
<strong style={{ fontSize: 16, color: '#10b981' }}>{t('common:save')}</strong>
</p>
<p style={{ margin: '4px 0', display: 'flex', alignItems: 'center' }}>
<code style={{ color: '#94a3b8', width: 180, display: 'inline-block' }}>booking:select_date</code>
<strong style={{ fontSize: 16, color: '#10b981' }}>{t('booking:select_date')}</strong>
</p>
</div>
</div>
{/* ─── Section B ────────────────────────────────────────────── */}
@@ -257,19 +285,22 @@ export default function I18nSample() {
</p>
<div style={{ display: 'flex', gap: 8, marginBottom: 24 }}>
<button onClick={resetTenant} style={btnStyle(activeTenant === 'default' ? '#16a34a' : '#475569')}>
<button
onClick={resetTenant}
style={btnStyle(activeTenant === 'default' ? '#16a34a' : '#475569', activeTenant === 'default')}
>
Default Company
</button>
<button
onClick={() => handleSimulateLogin('company-a')}
style={btnStyle(activeTenant === 'company-a' ? '#16a34a' : '#475569')}
style={btnStyle(activeTenant === 'company-a' ? '#16a34a' : '#475569', activeTenant === 'company-a')}
disabled={isFetchingConfig}
>
Simulate Login as Company A
</button>
<button
onClick={() => handleSimulateLogin('company-b')}
style={btnStyle(activeTenant === 'company-b' ? '#16a34a' : '#475569')}
style={btnStyle(activeTenant === 'company-b' ? '#16a34a' : '#475569', activeTenant === 'company-b')}
disabled={isFetchingConfig}
>
Simulate Login as Company B
@@ -282,14 +313,13 @@ export default function I18nSample() {
{/* Display the localized strings */}
<div style={{ background: '#1e293b', padding: 16, borderRadius: 8 }}>
<h4 style={{ color: '#cbd5e1', marginBottom: 12 }}>UI Result:</h4>
<h4 style={{ color: '#cbd5e1', marginBottom: 12 }}>UI Result (Tenant Overlay):</h4>
<table style={{ width: '100%', textAlign: 'left', borderCollapse: 'collapse' }}>
<tbody>
<tr style={{ borderBottom: '1px solid #334155' }}>
<th style={{ padding: 8, color: '#94a3b8' }}>Key</th>
<th style={{ padding: 8, color: '#94a3b8' }}>Value</th>
</tr>
{/* Type-safe keys from the common and booking namespaces */}
<tr style={{ borderBottom: '1px solid #334155' }}>
<td style={{ padding: 8 }}>
<code>booking:module_name</code>
@@ -308,18 +338,6 @@ export default function I18nSample() {
</td>
<td style={{ padding: 8, fontWeight: 'bold' }}>{t('booking:header.subtitle')}</td>
</tr>
<tr style={{ borderBottom: '1px solid #334155' }}>
<td style={{ padding: 8 }}>
<code>booking:select_date</code>
</td>
<td style={{ padding: 8, fontWeight: 'bold' }}>{t('booking:select_date')}</td>
</tr>
<tr>
<td style={{ padding: 8 }}>
<code>common:save</code>
</td>
<td style={{ padding: 8, fontWeight: 'bold' }}>{t('common:save')}</td>
</tr>
</tbody>
</table>
</div>