Skip to content

Commit 4a1e65f

Browse files
committed
added multi-lang
1 parent 7c2505b commit 4a1e65f

File tree

8 files changed

+589
-11
lines changed

8 files changed

+589
-11
lines changed

docs/website/api.html

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@
77
<link rel="stylesheet" href="css/styles.css">
88
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
99
<script>
10-
// Apply saved theme immediately to prevent flash
1110
(function() {
12-
const saved = localStorage.getItem('fula-docs-theme');
13-
if (saved) document.documentElement.setAttribute('data-theme', saved);
11+
const savedTheme = localStorage.getItem('fula-docs-theme');
12+
if (savedTheme) document.documentElement.setAttribute('data-theme', savedTheme);
13+
const savedLang = localStorage.getItem('fula-docs-language');
14+
if (savedLang && ['fa', 'ar'].includes(savedLang)) {
15+
document.documentElement.setAttribute('dir', 'rtl');
16+
document.documentElement.setAttribute('lang', savedLang);
17+
}
1418
})();
1519
</script>
1620
</head>
@@ -1173,6 +1177,8 @@ <h3>Permissions</h3>
11731177
</main>
11741178

11751179
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
1180+
<script src="js/i18n.js"></script>
1181+
<script src="js/translations.js"></script>
11761182
<script src="js/app.js"></script>
11771183
</body>
11781184
</html>

docs/website/css/styles.css

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -699,3 +699,118 @@ footer {
699699
color: #0969da;
700700
border-left-color: #0969da;
701701
}
702+
703+
/* ==================== Language Selector ==================== */
704+
.language-selector {
705+
position: relative;
706+
margin-right: 10px;
707+
}
708+
709+
.language-selector-btn {
710+
display: flex;
711+
align-items: center;
712+
gap: 6px;
713+
padding: 6px 10px;
714+
background: transparent;
715+
border: 1px solid var(--border-color);
716+
border-radius: 6px;
717+
color: var(--text-primary);
718+
cursor: pointer;
719+
font-size: 0.85rem;
720+
transition: all 0.2s;
721+
}
722+
723+
.language-selector-btn:hover {
724+
background: var(--bg-code);
725+
border-color: var(--accent-blue);
726+
}
727+
728+
.language-icon { font-size: 1rem; }
729+
.language-arrow { font-size: 0.6rem; opacity: 0.7; transition: transform 0.2s; }
730+
.language-dropdown.open + .language-selector-btn .language-arrow,
731+
.language-selector-btn[aria-expanded="true"] .language-arrow { transform: rotate(180deg); }
732+
733+
.language-dropdown {
734+
position: absolute;
735+
top: 100%;
736+
left: 0;
737+
margin-top: 4px;
738+
background: var(--bg-sidebar);
739+
border: 1px solid var(--border-color);
740+
border-radius: 8px;
741+
box-shadow: 0 8px 24px var(--shadow-color);
742+
min-width: 180px;
743+
max-height: 320px;
744+
overflow-y: auto;
745+
opacity: 0;
746+
visibility: hidden;
747+
transform: translateY(-10px);
748+
transition: all 0.2s;
749+
z-index: 1000;
750+
}
751+
752+
.language-dropdown.open {
753+
opacity: 1;
754+
visibility: visible;
755+
transform: translateY(0);
756+
}
757+
758+
.language-option {
759+
display: flex;
760+
flex-direction: column;
761+
align-items: flex-start;
762+
width: 100%;
763+
padding: 10px 14px;
764+
background: transparent;
765+
border: none;
766+
color: var(--text-primary);
767+
cursor: pointer;
768+
text-align: left;
769+
transition: background 0.15s;
770+
}
771+
772+
.language-option:hover {
773+
background: var(--bg-code);
774+
}
775+
776+
.language-option.active {
777+
background: rgba(88, 166, 255, 0.15);
778+
color: var(--accent-blue);
779+
}
780+
781+
.language-option .lang-native {
782+
font-weight: 500;
783+
font-size: 0.9rem;
784+
}
785+
786+
.language-option .lang-english {
787+
font-size: 0.75rem;
788+
color: var(--text-secondary);
789+
margin-top: 2px;
790+
}
791+
792+
.language-option.active .lang-english {
793+
color: var(--accent-blue);
794+
opacity: 0.8;
795+
}
796+
797+
/* RTL Support */
798+
body.rtl { direction: rtl; }
799+
body.rtl .sidebar { left: auto; right: 0; border-right: none; border-left: 1px solid var(--border-color); }
800+
body.rtl .content { margin-left: 0; margin-right: 280px; }
801+
body.rtl .nav-section li a { border-left: none; border-right: 3px solid transparent; padding-left: 20px; padding-right: 17px; }
802+
body.rtl .nav-section li a.active { border-right-color: var(--accent-blue); }
803+
body.rtl .language-dropdown { left: auto; right: 0; }
804+
body.rtl .language-option { text-align: right; }
805+
806+
/* Mobile language selector */
807+
.mobile-lang-selector .language-dropdown {
808+
right: 0;
809+
left: auto;
810+
}
811+
812+
@media (max-width: 768px) {
813+
body.rtl .content { margin-right: 0; }
814+
body.rtl .sidebar { transform: translateX(100%); }
815+
body.rtl .sidebar.open { transform: translateX(0); }
816+
}

docs/website/index.html

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,15 @@
88
<link rel="stylesheet" href="css/intro.css">
99
<script>
1010
(function() {
11-
const saved = localStorage.getItem('fula-docs-theme');
12-
if (saved) document.documentElement.setAttribute('data-theme', saved);
11+
// Load saved theme
12+
const savedTheme = localStorage.getItem('fula-docs-theme');
13+
if (savedTheme) document.documentElement.setAttribute('data-theme', savedTheme);
14+
// Load saved language direction (for RTL languages)
15+
const savedLang = localStorage.getItem('fula-docs-language');
16+
if (savedLang && ['fa', 'ar'].includes(savedLang)) {
17+
document.documentElement.setAttribute('dir', 'rtl');
18+
document.documentElement.setAttribute('lang', savedLang);
19+
}
1320
})();
1421
</script>
1522
</head>
@@ -653,6 +660,8 @@ <h3>Next Steps</h3>
653660
</footer>
654661
</main>
655662

663+
<script src="js/i18n.js"></script>
664+
<script src="js/translations.js"></script>
656665
<script src="js/app.js"></script>
657666
</body>
658667
</html>

docs/website/js/i18n.js

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Fula API Documentation - Internationalization (i18n) System
2+
// Languages: English, Chinese, Farsi, Arabic, German, Spanish, Hindi, French
3+
4+
const SUPPORTED_LANGUAGES = {
5+
en: { name: 'English', nativeName: 'English', dir: 'ltr' },
6+
zh: { name: 'Chinese', nativeName: '中文', dir: 'ltr' },
7+
fa: { name: 'Farsi', nativeName: 'فارسی', dir: 'rtl' },
8+
ar: { name: 'Arabic', nativeName: 'العربية', dir: 'rtl' },
9+
de: { name: 'German', nativeName: 'Deutsch', dir: 'ltr' },
10+
es: { name: 'Spanish', nativeName: 'Español', dir: 'ltr' },
11+
hi: { name: 'Hindi', nativeName: 'हिन्दी', dir: 'ltr' },
12+
fr: { name: 'French', nativeName: 'Français', dir: 'ltr' }
13+
};
14+
15+
const STORAGE_KEY = 'fula-docs-language';
16+
const DEFAULT_LANGUAGE = 'en';
17+
18+
class I18n {
19+
constructor() {
20+
this.translations = {};
21+
this.currentLanguage = this.getSavedLanguage() || this.detectBrowserLanguage() || DEFAULT_LANGUAGE;
22+
this.listeners = [];
23+
}
24+
25+
getSavedLanguage() {
26+
try { return localStorage.getItem(STORAGE_KEY); } catch (e) { return null; }
27+
}
28+
29+
detectBrowserLanguage() {
30+
const browserLang = (navigator.language || navigator.userLanguage || '').split('-')[0];
31+
return SUPPORTED_LANGUAGES[browserLang] ? browserLang : null;
32+
}
33+
34+
getLanguage() { return this.currentLanguage; }
35+
36+
setLanguage(lang) {
37+
if (!SUPPORTED_LANGUAGES[lang]) lang = DEFAULT_LANGUAGE;
38+
this.currentLanguage = lang;
39+
try { localStorage.setItem(STORAGE_KEY, lang); } catch (e) {}
40+
this.applyLanguage();
41+
this.listeners.forEach(cb => cb(lang));
42+
}
43+
44+
t(key, fallback = null) {
45+
const t = this.translations[this.currentLanguage];
46+
if (t && t[key]) return t[key];
47+
if (this.translations.en && this.translations.en[key]) return this.translations.en[key];
48+
return fallback || key;
49+
}
50+
51+
applyLanguage() {
52+
const langInfo = SUPPORTED_LANGUAGES[this.currentLanguage];
53+
document.documentElement.setAttribute('lang', this.currentLanguage);
54+
document.documentElement.setAttribute('dir', langInfo.dir);
55+
document.body.classList.toggle('rtl', langInfo.dir === 'rtl');
56+
57+
document.querySelectorAll('[data-i18n]').forEach(el => {
58+
const key = el.getAttribute('data-i18n');
59+
const translation = this.t(key);
60+
if (translation.includes('<')) el.innerHTML = translation;
61+
else el.textContent = translation;
62+
});
63+
64+
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
65+
el.placeholder = this.t(el.getAttribute('data-i18n-placeholder'));
66+
});
67+
68+
const selector = document.querySelector('.language-selector-current');
69+
if (selector) selector.textContent = langInfo.nativeName;
70+
}
71+
72+
onLanguageChange(callback) { this.listeners.push(callback); }
73+
getSupportedLanguages() { return SUPPORTED_LANGUAGES; }
74+
loadTranslations(t) { this.translations = t; }
75+
}
76+
77+
const i18n = new I18n();
78+
79+
function createLanguageSelector() {
80+
const selector = document.createElement('div');
81+
selector.className = 'language-selector';
82+
selector.innerHTML = `
83+
<button class="language-selector-btn" aria-label="Select language">
84+
<span class="language-icon">🌐</span>
85+
<span class="language-selector-current">${SUPPORTED_LANGUAGES[i18n.getLanguage()].nativeName}</span>
86+
<span class="language-arrow">▼</span>
87+
</button>
88+
<div class="language-dropdown">
89+
${Object.entries(SUPPORTED_LANGUAGES).map(([code, info]) => `
90+
<button class="language-option ${code === i18n.getLanguage() ? 'active' : ''}"
91+
data-lang="${code}" dir="${info.dir}">
92+
<span class="lang-native">${info.nativeName}</span>
93+
<span class="lang-english">${info.name}</span>
94+
</button>
95+
`).join('')}
96+
</div>
97+
`;
98+
99+
const btn = selector.querySelector('.language-selector-btn');
100+
const dropdown = selector.querySelector('.language-dropdown');
101+
102+
btn.addEventListener('click', (e) => { e.stopPropagation(); dropdown.classList.toggle('open'); });
103+
104+
selector.querySelectorAll('.language-option').forEach(option => {
105+
option.addEventListener('click', (e) => {
106+
e.stopPropagation();
107+
i18n.setLanguage(option.getAttribute('data-lang'));
108+
dropdown.classList.remove('open');
109+
selector.querySelectorAll('.language-option').forEach(opt => {
110+
opt.classList.toggle('active', opt.getAttribute('data-lang') === i18n.getLanguage());
111+
});
112+
});
113+
});
114+
115+
document.addEventListener('click', () => dropdown.classList.remove('open'));
116+
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') dropdown.classList.remove('open'); });
117+
118+
return selector;
119+
}
120+
121+
function initI18n() {
122+
const sidebarHeader = document.querySelector('.sidebar-header');
123+
if (sidebarHeader) {
124+
const langSelector = createLanguageSelector();
125+
const themeToggle = sidebarHeader.querySelector('.theme-toggle');
126+
if (themeToggle) themeToggle.insertAdjacentElement('beforebegin', langSelector);
127+
else sidebarHeader.appendChild(langSelector);
128+
}
129+
130+
const mobileHeaderActions = document.querySelector('.mobile-header-actions');
131+
if (mobileHeaderActions) {
132+
const mobileLangSelector = createLanguageSelector();
133+
mobileLangSelector.classList.add('mobile-lang-selector');
134+
mobileHeaderActions.insertBefore(mobileLangSelector, mobileHeaderActions.firstChild);
135+
}
136+
137+
i18n.applyLanguage();
138+
}
139+
140+
// Auto-init when DOM is ready
141+
if (document.readyState === 'loading') {
142+
document.addEventListener('DOMContentLoaded', initI18n);
143+
} else {
144+
initI18n();
145+
}

0 commit comments

Comments
 (0)