Skip to content

Commit 450f6fa

Browse files
committed
feat: restore ip checking auto-detection mechanism
1 parent 439315d commit 450f6fa

1 file changed

Lines changed: 258 additions & 19 deletions

File tree

src/pages/index.js

Lines changed: 258 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,117 @@ const PYPI_JSON = `https://pypi.org/pypi/${PYPI_PROJECT}/json`;
2222
export const defaultLightThemeOption = EditorView.theme({ '&': { backgroundColor: 'whitesmoke' } }, { dark: false });
2323
import styles from './index.module.css';
2424

25-
// languages (kept short here; extend as needed)
25+
// Array of Languages on right side of code editor
2626
const languages = [
27-
{ id: 'EN', code3: 'eng', code2: 'en', name: 'English', i18nName: 'English', fontFamily: "Hack, 'Courier New', monospace" },
28-
{ id: 'HI', default: true, code3: 'hin', code2: 'hi', name: 'Hindi', i18nName: 'Hindi', fontFamily: "Hack, 'Courier New', monospace" },
29-
{ id: 'UR', code3: 'urd', code2: 'ur', name: 'Urdu', i18nName: 'اردو', direction: 'rtl', fontFamily: "Hack, 'Courier New', monospace" },
30-
{ id: 'FR', code3: 'fra', code2: 'fr', name: 'French', i18nName: 'Français', fontFamily: "Hack, 'Courier New', monospace" },
31-
];
27+
{
28+
id: "CS",
29+
code3: "ces",
30+
code2: "cs",
31+
name: "Czech",
32+
i18nName: "Čeština",
33+
fontFamily: "'Roboto Mono'",
34+
toEnglishDict: "'languages/cs/default.yaml'",
35+
},
36+
{
37+
id: "DE",
38+
code3: "deu",
39+
code2: "de",
40+
name: "German",
41+
i18nName: "Deutsch",
42+
fontFamily: "'Roboto Mono'",
43+
toEnglishDict: "'languages/de/default.yaml'",
44+
},
45+
{
46+
id: "UR",
47+
code3: "urd",
48+
code2: "ur",
49+
name: "Urdu",
50+
i18nName: "اردو",
51+
direction: "rtl",
52+
fontFamily: "'Roboto Mono'",
53+
toEnglishDict: "'languages/ur/default.yaml'",
54+
fontWeights: "bold",
55+
style: {
56+
direction: "rtl"
57+
}
58+
},
59+
{
60+
id: "HI",
61+
default: true,
62+
code3: "hin",
63+
code2: "hi",
64+
name: "Hindi",
65+
i18nName: "Hindi",
66+
fontFamily: "'Roboto Mono'",
67+
toEnglishDict: "'languages/hi/default.yaml'",
68+
},
69+
{
70+
id: "EN",
71+
code3: "eng",
72+
code2: "en",
73+
name: "English",
74+
i18nName: "English",
75+
fontFamily: "'Roboto Mono'",
76+
},
77+
{
78+
id: "GA",
79+
code3: "gle",
80+
code2: "ga",
81+
name: "Irish",
82+
i18nName: "Gaeilge",
83+
fontFamily: "'Roboto Mono'",
84+
toEnglishDict: "'languages/ga/default.yaml'",
85+
},
86+
{
87+
id: "KO",
88+
code3: "kor",
89+
code2: "ko",
90+
name: "Korean",
91+
i18nName: "한국어",
92+
fontFamily: "'Roboto Mono'",
93+
toEnglishDict: "'languages/ko/default.yaml'",
94+
},
95+
{
96+
id: "FR",
97+
code3: "fra",
98+
code2: "fr",
99+
name: "French",
100+
i18nName: "Français",
101+
fontFamily: "'Roboto Mono'",
102+
toEnglishDict: "'languages/fr/default.yaml'",
103+
},
104+
{
105+
id: "TR",
106+
code3: "tur",
107+
code2: "tr",
108+
name: "Turkish",
109+
i18nName: "Türkçe",
110+
fontFamily: "'Roboto Mono'",
111+
toEnglishDict: "'languages/tr/default.yaml'",
112+
},
113+
{
114+
id: "HU",
115+
code3: "hun",
116+
code2: "hu",
117+
name: "Hungarian",
118+
i18nName: "Magyar",
119+
fontFamily: "'Roboto Mono'",
120+
toEnglishDict: "'languages/hu/default.yaml'",
121+
},
122+
// Added Emoji
123+
{
124+
id: "EMOJI",
125+
code3: "emo",
126+
code2: "emoji",
127+
name: "The Emoji Language",
128+
i18nName: "😀😃😄😁😆",
129+
fontFamily: "'Segoe UI Emoji', 'Apple Color Emoji', 'Noto Color Emoji', sans-serif",
130+
toEnglishDict: "'languages/emoji/default.yaml'",
131+
style: {
132+
fontSize: "1.2em"
133+
}
134+
},
135+
]
32136

33137
const initialCodes = [
34138
{ id: 'hello_world', name: ' Simple Hello World', en: `print("Hello world!")` },
@@ -177,23 +281,34 @@ export default function Home() {
177281
const isBrowser = useIsBrowser();
178282
const { country } = useGeoLocation();
179283

284+
// States
180285
const [editorCode, setEditorCode] = useState(initialCodes[0].en);
181286
const [code, setCode] = useState(initialCodes[0].en);
182287
const [translatedCode, setTranslatedCode] = useState('');
183288
const [isWaitingForCode, setIsWaitingForCode] = useState(false);
184289
const [isDetected, setIsDetected] = useState(false);
185-
const [sourceLanguage, setSourceLanguage] = useState(languages.find(l => l.code2 === 'en'));
186-
const [targetLanguage, setTargetLanguage] = useState(languages.find(l => l.default));
290+
291+
// find Urdu by code2 'ur' (you asked for 'ur' key)
292+
const urduLang = languages.find(l => l.code2 === 'ur') || languages.find(l => l.id === 'UR');
293+
const defaultSource = languages.find(l => l.code2 === 'en') || languages.find(l => l.id === 'EN');
294+
295+
// track whether the user manually changed the target language (Option A)
296+
const userManuallySelectedTarget = useRef(false);
297+
// track if auto-detection has already run once
298+
const autoDetectDone = useRef(false);
299+
// track if URL had a tgt param on load
300+
const [urlHasTgt, setUrlHasTgt] = useState(false);
301+
302+
const [sourceLanguage, setSourceLanguage] = useState(defaultSource);
303+
// default target is Urdu per your request
304+
const [targetLanguage, setTargetLanguage] = useState(urduLang || languages.find(l => l.default));
305+
187306
const [loadingPyscript, setLoadingPyscript] = useState(true);
188307
const [wheelUrl, setWheelUrl] = useState(null);
189308

190-
// existing read-only snackbar (when overlay clicked)
309+
// existing snackbars / dialogs
191310
const [snackOpen, setSnackOpen] = useState(false);
192-
193-
// snackbar for copy confirmations
194311
const [copySnack, setCopySnack] = useState({ open: false, msg: '', anchorOrigin: {} });
195-
196-
// share dialog state (fallback)
197312
const [shareDialogOpen, setShareDialogOpen] = useState(false);
198313
const [shareUrlValue, setShareUrlValue] = useState('');
199314

@@ -213,6 +328,9 @@ export default function Home() {
213328
if (tgt) {
214329
const found = languages.find(l => l.code2 === tgt || l.id === tgt);
215330
if (found) setTargetLanguage(found);
331+
setUrlHasTgt(true); // mark that URL override exists
332+
// treat URL param as an explicit manual selection
333+
userManuallySelectedTarget.current = true;
216334
}
217335
}, [isBrowser]);
218336

@@ -242,8 +360,16 @@ export default function Home() {
242360
return () => clearTimeout(t);
243361
}, [editorCode]);
244362

245-
// swap languages & swap editor text by swapping React state (fixes your issue)
363+
// Helper: mark that user manually selected target (blocks future auto-detect)
364+
const markUserManualTarget = (alsoClearDetected = false) => {
365+
userManuallySelectedTarget.current = true;
366+
if (alsoClearDetected) setIsDetected(false);
367+
};
368+
369+
// swap languages & swap editor text by swapping React state (user action -> manual selection)
246370
const swapLanguages = () => {
371+
// mark manual because the user clicked swap
372+
markUserManualTarget();
247373
// swap language objects
248374
setSourceLanguage(prevSource => { const oldSource = prevSource; setTargetLanguage(oldSource); return targetLanguage; });
249375
// swap code content between editor and translated output (we maintain code as the source text)
@@ -340,6 +466,98 @@ if data_el is not None:
340466
return () => observer.disconnect();
341467
}, [isBrowser]);
342468

469+
// ---------- AUTO-DETECT LOGIC (Option A: only on first load) ----------
470+
// Hook: browser language detection (first-tier after URL param)
471+
useEffect(() => {
472+
if (!isBrowser) return;
473+
if (autoDetectDone.current) return;
474+
if (urlHasTgt) {
475+
autoDetectDone.current = true;
476+
return;
477+
}
478+
if (userManuallySelectedTarget.current) {
479+
autoDetectDone.current = true;
480+
return;
481+
}
482+
483+
// Try navigator language first (but ignore English)
484+
try {
485+
const userLang = navigator.language || navigator.userLanguage || '';
486+
const _languageCode = (userLang || '').split('-')[0].toLowerCase();
487+
if (_languageCode && _languageCode !== 'en') {
488+
const idx = languages.findIndex(l => l.code2 === _languageCode);
489+
if (idx > -1) {
490+
setTargetLanguage(languages[idx]);
491+
setIsDetected(true);
492+
autoDetectDone.current = true;
493+
return;
494+
}
495+
}
496+
} catch (e) {
497+
console.warn('browser language detection failed', e);
498+
}
499+
// If browser didn't produce a non-English match, defer to country-based detection below
500+
}, [isBrowser, urlHasTgt]);
501+
502+
// Hook: country (IP) -> restcountries lookup as a fallback after browser language
503+
useEffect(() => {
504+
if (!isBrowser) return;
505+
if (autoDetectDone.current) return;
506+
if (urlHasTgt) { autoDetectDone.current = true; return; }
507+
if (userManuallySelectedTarget.current) { autoDetectDone.current = true; return; }
508+
if (!country) return; // wait until react-ipgeolocation provides country code
509+
510+
// Use restcountries to find languages for the alpha country code
511+
(async () => {
512+
try {
513+
const res = await fetch(`https://restcountries.com/v3.1/alpha/${country}`);
514+
if (!res.ok) throw new Error('restcountries fetch failed');
515+
const result = await res.json();
516+
if (!Array.isArray(result) || result.length === 0) {
517+
throw new Error('restcountries returned no data');
518+
}
519+
// `languages` is an object with keys like "eng","urd" or sometimes ISO 639-1 like "en","ur".
520+
const countryLangsObj = result[0].languages || {};
521+
const countryLangCodes = Object.keys(countryLangsObj || {}); // keys might be 'urd','eng' or 'ur','en' etc.
522+
523+
// Attempt to find a supported language by matching code2 or code3, while skipping English
524+
let foundLang = null;
525+
for (const c of countryLangCodes) {
526+
if (!c) continue;
527+
const keyLower = String(c).toLowerCase();
528+
if (keyLower === 'eng' || keyLower === 'en') continue; // skip English
529+
530+
// check code2 match first (e.g., 'ur')
531+
const byCode2 = languages.find(l => l.code2 && l.code2.toLowerCase() === keyLower);
532+
if (byCode2) { foundLang = byCode2; break; }
533+
// then check code3 match (e.g., 'urd' or 'fra')
534+
const byCode3 = languages.find(l => l.code3 && l.code3.toLowerCase() === keyLower);
535+
if (byCode3) { foundLang = byCode3; break; }
536+
}
537+
538+
if (foundLang) {
539+
setTargetLanguage(foundLang);
540+
setIsDetected(true);
541+
} else {
542+
// If country languages did not yield a non-English supported language, fallback to Urdu
543+
if (urduLang) {
544+
setTargetLanguage(urduLang);
545+
setIsDetected(false);
546+
}
547+
}
548+
} catch (e) {
549+
console.warn('country-based detection failed', e);
550+
if (urduLang) {
551+
setTargetLanguage(urduLang);
552+
setIsDetected(false);
553+
}
554+
} finally {
555+
autoDetectDone.current = true;
556+
}
557+
})();
558+
}, [country, isBrowser, urlHasTgt, urduLang]);
559+
// ---------- END AUTO-DETECT LOGIC ----------
560+
343561
// Snackbar helper (read-only)
344562
const showReadOnlySnack = () => {
345563
setSnackOpen(true);
@@ -417,6 +635,23 @@ if data_el is not None:
417635
}
418636
};
419637

638+
// UI change handlers (ensure manual selection blocks further auto-detects)
639+
const handleSourceSelect = (e) => {
640+
const found = languages.find(l => l.id === e.target.value);
641+
if (found) setSourceLanguage(found);
642+
};
643+
644+
const handleTargetSelect = (e) => {
645+
const found = languages.find(l => l.id === e.target.value);
646+
if (found) {
647+
setTargetLanguage(found);
648+
// mark that the user manually changed the target language — Option A
649+
markUserManualTarget();
650+
// clear detected flag because user explicitly chose
651+
setIsDetected(false);
652+
}
653+
};
654+
420655
return (
421656
<Layout title={`${siteConfig.title} | Programming for everyone`} description="Write Python in any human language. Can't find yours? Easily contribute.">
422657
<Head>
@@ -458,7 +693,7 @@ if data_el is not None:
458693
{/* Left editor container (position: relative for floating button) */}
459694
<div style={{ flex: 1, position: 'relative' }}>
460695
<Box width="250px">
461-
<TextField label="From" fullWidth select onChange={(e) => { const found = languages.find(l => l.id === e.target.value); if (found) { setSourceLanguage(found); setIsDetected(false); } }} value={sourceLanguage?.id}>
696+
<TextField label="From" fullWidth select onChange={handleSourceSelect} value={sourceLanguage?.id}>
462697
{languages.map((l) => <MenuItem key={l.id} value={l.id}>{l.name}</MenuItem>)}
463698
</TextField>
464699
</Box>
@@ -481,14 +716,16 @@ if data_el is not None:
481716
</IconButton>
482717
</Tooltip>
483718

484-
<IDE id="python-code-editor1" value={editorCode} mode="python" onChange={(text) => { setEditorCode(text); }} height={'240px'} style={{ margin: 12, fontFamily: sourceLanguage?.fontFamily }} />
719+
<div style={{ position: 'relative', direction: sourceLanguage?.direction || 'ltr' }} direction={sourceLanguage?.direction || 'ltr'}>
720+
<IDE id="python-code-editor1" value={editorCode} mode="python" onChange={(text) => { setEditorCode(text); }} height={'240px'} style={{ borderRadius: '0.4rem', overflow: 'hidden', margin: 12, fontFamily: sourceLanguage?.fontFamily }} />
721+
</div>
485722
</div>
486723

487724
<Button sx={{ height: 'fit-content', width: { xs: '100%', md: '5%' }, m: { xs: '10px 0', md: 0 } }} onClick={swapLanguages}>&#8644;</Button>
488725

489726
<div style={{ flex: 1, marginLeft: 12, position: 'relative' }}>
490727
<Box display="flex" alignItems="center" justifyContent="space-between">
491-
<TextField select label="To" fullWidth onChange={(e) => { const found = languages.find(l => l.id === e.target.value); if (found) setTargetLanguage(found); }} value={targetLanguage?.id} sx={{ maxWidth: 250 }}>
728+
<TextField select label="To" fullWidth onChange={handleTargetSelect} value={targetLanguage?.id} sx={{ maxWidth: 250 }}>
492729
{languages.map((l) => <MenuItem key={l.id} value={l.id}>{l.name}{targetLanguage?.id === l.id ? isDetected ? ' - detected' : '' : ''}</MenuItem>)}
493730
</TextField>
494731
</Box>
@@ -512,8 +749,8 @@ if data_el is not None:
512749
</Tooltip>
513750

514751
{/* Right-hand editor: real CodeMirror, readOnly. We render an overlay to catch interactions and show snackbar */}
515-
<div style={{ position: 'relative' }}>
516-
<IDE id="python-code-editor2" mode="python" height={'240px'} readOnly={true} value={translatedCode} basicSetup={{ direction: targetLanguage?.direction || 'ltr' }} style={{ margin: 12, whiteSpace: 'pre', fontFamily: targetLanguage?.fontFamily }} handleReadOnlyTyping={showReadOnlySnack} />
752+
<div style={{ position: 'relative', direction: targetLanguage?.direction || 'ltr' }} direction={targetLanguage?.direction || 'ltr'}>
753+
<IDE id="python-code-editor2" mode="python" height={'240px'} readOnly={true} value={translatedCode} basicSetup={{ direction: targetLanguage?.direction || 'ltr' }} style={{ borderRadius: '0.4rem', overflow: 'hidden', margin: 12, whiteSpace: 'pre', fontFamily: targetLanguage?.fontFamily }} handleReadOnlyTyping={showReadOnlySnack} />
517754

518755
{/* transparent overlay to block interactions and show tooltip/snackbar on attempt */}
519756
<div
@@ -579,6 +816,8 @@ if data_el is not None:
579816
);
580817
}
581818

819+
820+
582821
const MaterialThemeWrapper = ({ children }) => {
583822
const { colorMode } = useColorMode(); const isDarkTheme = colorMode === 'dark';
584823
const theme = React.useMemo(() => createTheme({ palette: { mode: isDarkTheme ? 'dark' : 'light' } }), [isDarkTheme]);

0 commit comments

Comments
 (0)