refactor: enhance i18n handling by implementing state management for active language and optimizing resource loading
This commit is contained in:
@@ -1,48 +1,87 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation, changeLanguage, i18n } from '@repo/core-i18n';
|
||||
|
||||
// Decentralized locale imports
|
||||
import homeId from '../locales/id/home.json';
|
||||
import homeEn from '../locales/en/home.json';
|
||||
|
||||
export default function I18nLandingSample() {
|
||||
const { t } = useTranslation(['common', 'home']);
|
||||
// Bendera penanda statis di level modul (default: false)
|
||||
let isHomeDictLoaded = false;
|
||||
|
||||
// 1. Lazy-load the 'home' namespace when the module mounts
|
||||
useEffect(() => {
|
||||
export default function I18nLandingSample() {
|
||||
// 1. Eksekusi SINKRONUS tepat sebelum render pertama (hanya berjalan 1x)
|
||||
if (!isHomeDictLoaded) {
|
||||
i18n.addResourceBundle('id', 'home', homeId, true, false);
|
||||
i18n.addResourceBundle('en', 'home', homeEn, true, false);
|
||||
isHomeDictLoaded = true; // Kunci benderanya agar tidak jalan lagi saat re-render
|
||||
}
|
||||
|
||||
// 2. Sekarang useTranslation akan melihat kamus yang sudah siap
|
||||
const { t } = useTranslation(['common', 'home']);
|
||||
|
||||
const [activeLang, setActiveLang] = useState(i18n.language);
|
||||
|
||||
useEffect(() => {
|
||||
const handleLangChange = (lng: string) => setActiveLang(lng);
|
||||
i18n.on('languageChanged', handleLangChange);
|
||||
|
||||
return () => {
|
||||
i18n.off('languageChanged', handleLangChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 2. Change language without syncCallback to prove decoupling
|
||||
const setLanguage = (lng: string) => {
|
||||
// We intentionally omit the second argument (syncCallback)
|
||||
// because the landing page is public and doesn't need backend syncing.
|
||||
changeLanguage(lng).catch(console.error);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: 40, textAlign: 'center', backgroundColor: '#f8fafc', color: '#0f172a' }}>
|
||||
<h1 style={{ fontSize: 36, fontWeight: 'bold', marginBottom: 16 }}>
|
||||
{t('home:welcome')}
|
||||
</h1>
|
||||
|
||||
<h1 style={{ fontSize: 36, fontWeight: 'bold', marginBottom: 16 }}>{t('home:welcome')}</h1>
|
||||
|
||||
<div style={{ marginBottom: 32 }}>
|
||||
<button
|
||||
<button
|
||||
onClick={() => setLanguage('id')}
|
||||
style={{ padding: '8px 16px', marginRight: 8, cursor: 'pointer', borderRadius: 4, background: '#3b82f6', color: 'white', border: 'none' }}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
marginRight: 8,
|
||||
cursor: 'pointer',
|
||||
borderRadius: 4,
|
||||
background: activeLang === 'id' ? '#1d4ed8' : '#93c5fd',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
fontWeight: activeLang === 'id' ? 'bold' : 'normal',
|
||||
}}
|
||||
>
|
||||
Bahasa Indonesia
|
||||
</button>
|
||||
<button
|
||||
<button
|
||||
onClick={() => setLanguage('en')}
|
||||
style={{ padding: '8px 16px', cursor: 'pointer', borderRadius: 4, background: '#3b82f6', color: 'white', border: 'none' }}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
cursor: 'pointer',
|
||||
borderRadius: 4,
|
||||
background: activeLang === 'en' ? '#1d4ed8' : '#93c5fd',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
fontWeight: activeLang === 'en' ? 'bold' : 'normal',
|
||||
}}
|
||||
>
|
||||
English
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button style={{ padding: '16px 32px', fontSize: 18, fontWeight: 'bold', cursor: 'pointer', borderRadius: 8, background: '#10b981', color: 'white', border: 'none' }}>
|
||||
<button
|
||||
style={{
|
||||
padding: '16px 32px',
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
cursor: 'pointer',
|
||||
borderRadius: 8,
|
||||
background: '#10b981',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
}}
|
||||
>
|
||||
{t('home:cta')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user