mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-17 17:34:39 +08:00
feat(i18n): add Indonesian (Bahasa Indonesia) language support (#1399)
- Add 'id' to Language type in translations.ts - Add ~1000 Indonesian translation keys covering all UI sections - Update LanguageContext to persist 'id' in localStorage - Add ID button to Header.tsx language toggle - Add �� option to HeaderBar.tsx desktop dropdown and mobile toggle - Add Indonesian translations to inline text objects in LoginRequiredOverlay, StrategyMarketPage, PositionHistory Closes #XX
This commit is contained in:
committed by
GitHub
parent
285053b7a4
commit
3358c5a53e
@@ -57,6 +57,17 @@ export function Header({ simple = false }: HeaderProps) {
|
|||||||
>
|
>
|
||||||
EN
|
EN
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setLanguage('id')}
|
||||||
|
className="px-3 py-1.5 rounded text-xs font-semibold transition-all"
|
||||||
|
style={
|
||||||
|
language === 'id'
|
||||||
|
? { background: '#F0B90B', color: '#000' }
|
||||||
|
: { background: 'transparent', color: '#848E9C' }
|
||||||
|
}
|
||||||
|
>
|
||||||
|
ID
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -99,8 +99,8 @@ export default function HeaderBar({
|
|||||||
{(() => {
|
{(() => {
|
||||||
// Define all navigation tabs
|
// Define all navigation tabs
|
||||||
const navTabs: { page: Page; path: string; label: string; requiresAuth: boolean }[] = [
|
const navTabs: { page: Page; path: string; label: string; requiresAuth: boolean }[] = [
|
||||||
{ page: 'data', path: '/data', label: language === 'zh' ? '数据' : 'Data', requiresAuth: false },
|
{ page: 'data', path: '/data', label: language === 'zh' ? '数据' : language === 'id' ? 'Data' : 'Data', requiresAuth: false },
|
||||||
{ page: 'strategy-market', path: '/strategy-market', label: language === 'zh' ? '策略市场' : 'Market', requiresAuth: true },
|
{ page: 'strategy-market', path: '/strategy-market', label: language === 'zh' ? '策略市场' : language === 'id' ? 'Pasar' : 'Market', requiresAuth: true },
|
||||||
{ page: 'traders', path: '/traders', label: t('configNav', language), requiresAuth: true },
|
{ page: 'traders', path: '/traders', label: t('configNav', language), requiresAuth: true },
|
||||||
{ page: 'trader', path: '/dashboard', label: t('dashboardNav', language), requiresAuth: true },
|
{ page: 'trader', path: '/dashboard', label: t('dashboardNav', language), requiresAuth: true },
|
||||||
{ page: 'strategy', path: '/strategy', label: t('strategyNav', language), requiresAuth: true },
|
{ page: 'strategy', path: '/strategy', label: t('strategyNav', language), requiresAuth: true },
|
||||||
@@ -259,7 +259,7 @@ export default function HeaderBar({
|
|||||||
className="flex items-center gap-2 px-3 py-2 rounded transition-colors text-nofx-text-muted hover:bg-white/5"
|
className="flex items-center gap-2 px-3 py-2 rounded transition-colors text-nofx-text-muted hover:bg-white/5"
|
||||||
>
|
>
|
||||||
<span className="text-lg">
|
<span className="text-lg">
|
||||||
{language === 'zh' ? '🇨🇳' : '🇺🇸'}
|
{language === 'zh' ? '🇨🇳' : language === 'id' ? '🇮🇩' : '🇺🇸'}
|
||||||
</span>
|
</span>
|
||||||
<ChevronDown className="w-4 h-4" />
|
<ChevronDown className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
@@ -288,6 +288,17 @@ export default function HeaderBar({
|
|||||||
<span className="text-base">🇺🇸</span>
|
<span className="text-base">🇺🇸</span>
|
||||||
<span className="text-sm">English</span>
|
<span className="text-sm">English</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
onLanguageChange?.('id')
|
||||||
|
setLanguageDropdownOpen(false)
|
||||||
|
}}
|
||||||
|
className={`w-full flex items-center gap-2 px-3 py-2 transition-colors text-nofx-text-muted hover:text-white
|
||||||
|
${language === 'id' ? 'bg-nofx-gold/10' : 'hover:bg-white/5'}`}
|
||||||
|
>
|
||||||
|
<span className="text-base">🇮🇩</span>
|
||||||
|
<span className="text-sm">Bahasa</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -329,8 +340,8 @@ export default function HeaderBar({
|
|||||||
<div className="flex flex-col gap-6 mb-12">
|
<div className="flex flex-col gap-6 mb-12">
|
||||||
{(() => {
|
{(() => {
|
||||||
const navTabs: { page: Page; path: string; label: string; requiresAuth: boolean }[] = [
|
const navTabs: { page: Page; path: string; label: string; requiresAuth: boolean }[] = [
|
||||||
{ page: 'data', path: '/data', label: language === 'zh' ? '数据' : 'Data', requiresAuth: false },
|
{ page: 'data', path: '/data', label: language === 'zh' ? '数据' : language === 'id' ? 'Data' : 'Data', requiresAuth: false },
|
||||||
{ page: 'strategy-market', path: '/strategy-market', label: language === 'zh' ? '策略市场' : 'Market', requiresAuth: true },
|
{ page: 'strategy-market', path: '/strategy-market', label: language === 'zh' ? '策略市场' : language === 'id' ? 'Pasar' : 'Market', requiresAuth: true },
|
||||||
{ page: 'traders', path: '/traders', label: t('configNav', language), requiresAuth: true },
|
{ page: 'traders', path: '/traders', label: t('configNav', language), requiresAuth: true },
|
||||||
{ page: 'trader', path: '/dashboard', label: t('dashboardNav', language), requiresAuth: true },
|
{ page: 'trader', path: '/dashboard', label: t('dashboardNav', language), requiresAuth: true },
|
||||||
{ page: 'strategy', path: '/strategy', label: t('strategyNav', language), requiresAuth: true },
|
{ page: 'strategy', path: '/strategy', label: t('strategyNav', language), requiresAuth: true },
|
||||||
@@ -429,7 +440,7 @@ export default function HeaderBar({
|
|||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
{/* Lang Switcher */}
|
{/* Lang Switcher */}
|
||||||
<div className="flex bg-zinc-900 rounded-lg p-1 border border-zinc-800">
|
<div className="flex bg-zinc-900 rounded-lg p-1 border border-zinc-800">
|
||||||
{['zh', 'en'].map((lang) => (
|
{['zh', 'en', 'id'].map((lang) => (
|
||||||
<button
|
<button
|
||||||
key={lang}
|
key={lang}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -441,7 +452,7 @@ export default function HeaderBar({
|
|||||||
: 'text-zinc-500'
|
: 'text-zinc-500'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{lang === 'zh' ? 'CN' : 'EN'}
|
{lang === 'zh' ? 'CN' : lang === 'id' ? 'ID' : 'EN'}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -40,6 +40,20 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ
|
|||||||
login: 'EXECUTE LOGIN',
|
login: 'EXECUTE LOGIN',
|
||||||
register: 'REGISTER NEW ID',
|
register: 'REGISTER NEW ID',
|
||||||
later: 'ABORT'
|
later: 'ABORT'
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
title: 'AKSES SISTEM DITOLAK',
|
||||||
|
subtitle: featureName ? `Modul "${featureName}" memerlukan hak akses lebih tinggi` : 'Otorisasi diperlukan untuk modul ini',
|
||||||
|
description: 'Inisialisasi protokol autentikasi untuk membuka kemampuan sistem penuh: konfigurasi Trader AI, aliran data Pasar Strategi, dan inti Simulasi Backtest.',
|
||||||
|
benefits: [
|
||||||
|
'Kontrol Trader AI',
|
||||||
|
'Pasar Strategi HFT',
|
||||||
|
'Mesin Backtest Historis',
|
||||||
|
'Visualisasi Sistem Penuh'
|
||||||
|
],
|
||||||
|
login: 'JALANKAN LOGIN',
|
||||||
|
register: 'DAFTAR ID BARU',
|
||||||
|
later: 'BATALKAN'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect, useMemo } from 'react'
|
import { useState, useEffect, useMemo } from 'react'
|
||||||
import { api } from '../lib/api'
|
import { api } from '../lib/api'
|
||||||
import { useLanguage } from '../contexts/LanguageContext'
|
import { useLanguage } from '../contexts/LanguageContext'
|
||||||
import { t } from '../i18n/translations'
|
import { t, type Language } from '../i18n/translations'
|
||||||
import { MetricTooltip } from './MetricTooltip'
|
import { MetricTooltip } from './MetricTooltip'
|
||||||
import { formatPrice, formatQuantity } from '../utils/format'
|
import { formatPrice, formatQuantity } from '../utils/format'
|
||||||
import type {
|
import type {
|
||||||
@@ -152,7 +152,7 @@ function SymbolStatsRow({ stat }: { stat: SymbolStats }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Direction Stats Card
|
// Direction Stats Card
|
||||||
function DirectionStatsCard({ stat, language }: { stat: DirectionStats; language: 'en' | 'zh' }) {
|
function DirectionStatsCard({ stat, language }: { stat: DirectionStats; language: Language }) {
|
||||||
const isLong = (stat.side || '').toLowerCase() === 'long'
|
const isLong = (stat.side || '').toLowerCase() === 'long'
|
||||||
const iconColor = isLong ? '#0ECB81' : '#F6465D'
|
const iconColor = isLong ? '#0ECB81' : '#F6465D'
|
||||||
const totalPnl = stat.total_pnl || 0
|
const totalPnl = stat.total_pnl || 0
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export function LanguageProvider({ children }: { children: ReactNode }) {
|
|||||||
// Initialize language from localStorage or default to English
|
// Initialize language from localStorage or default to English
|
||||||
const [language, setLanguage] = useState<Language>(() => {
|
const [language, setLanguage] = useState<Language>(() => {
|
||||||
const saved = localStorage.getItem('language')
|
const saved = localStorage.getItem('language')
|
||||||
return saved === 'en' || saved === 'zh' ? saved : 'en'
|
return saved === 'en' || saved === 'zh' || saved === 'id' ? saved : 'en'
|
||||||
})
|
})
|
||||||
|
|
||||||
// Save language to localStorage whenever it changes
|
// Save language to localStorage whenever it changes
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -158,6 +158,32 @@ export function StrategyMarketPage() {
|
|||||||
shareYours: 'UPLOAD_STRATEGY',
|
shareYours: 'UPLOAD_STRATEGY',
|
||||||
makePublic: 'PUBLISH',
|
makePublic: 'PUBLISH',
|
||||||
loading: 'INITIALIZING...'
|
loading: 'INITIALIZING...'
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
title: 'PASAR STRATEGI',
|
||||||
|
subtitle: 'DATABASE STRATEGI GLOBAL',
|
||||||
|
description: 'Temukan, analisis, dan kloning algoritma trading berperforma tinggi',
|
||||||
|
search: 'CARI PARAMETER...',
|
||||||
|
all: 'SEMUA PROTOKOL',
|
||||||
|
popular: 'TREN',
|
||||||
|
recent: 'TERBARU',
|
||||||
|
myStrategies: 'PERPUSTAKAAN SAYA',
|
||||||
|
noStrategies: 'TIDAK ADA SINYAL',
|
||||||
|
noStrategiesDesc: 'Tidak ada sinyal strategis terdeteksi pada frekuensi ini',
|
||||||
|
author: 'OPERATOR',
|
||||||
|
createdAt: 'TIMESTAMP',
|
||||||
|
viewConfig: 'DEKRIPSI CONFIG',
|
||||||
|
hideConfig: 'ENKRIPSI',
|
||||||
|
copyConfig: 'KLON CONFIG',
|
||||||
|
copied: 'DISALIN',
|
||||||
|
configHidden: 'TERENKRIPSI',
|
||||||
|
configHiddenDesc: 'Parameter konfigurasi terenkripsi',
|
||||||
|
indicators: 'INDIKATOR',
|
||||||
|
maxPositions: 'BATAS_POS',
|
||||||
|
maxLeverage: 'LEV_MAKS',
|
||||||
|
shareYours: 'UNGGAH_STRATEGI',
|
||||||
|
makePublic: 'PUBLIKASI',
|
||||||
|
loading: 'MENGINISIALISASI...'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user