1- import io , re
1+ import io , re , os
2+ import json as _json
3+ from html .parser import HTMLParser
24
35BASE = "https://astroscan-lab.github.io/astroscan_bot"
6+ ALL_LANGS = ["en" , "ru" , "es" , "hi" ]
47
58LANGS = {
69 "ru" : {
5356 },
5457}
5558
56- import json as _json
59+ EN_HOWTO_STEPS = [
60+ "Answer 10 questions about your behavior." ,
61+ "Receive your Chinese zodiac sign and archetype." ,
62+ "Choose a partner's sign and context (love/friendship/work)." ,
63+ "See compatibility percentage and personalized insights." ,
64+ ]
65+ EN_PRODUCT_DESC = ("Free online test to discover your Chinese zodiac sign, behavioral archetype, "
66+ "and compatibility. No registration required. Compare Western and Vedic astrology. "
67+ "Based on Swiss Ephemeris calculations." )
68+ EN_HOWTO_NAME = "How to use AstroScan"
69+
5770
5871def clean_faq_text (s ):
5972 s = re .sub (r'<[^>]+>' , '' , s )
6073 s = s .replace ('&' , '&' ).replace ('"' , '"' ).replace (''' , "'" )
6174 return s .strip ()
6275
76+
6377def extract_faq (html , lang ):
6478 pattern = re .compile (
6579 r'<details data-lang="%s"[^>]*>\s*<summary[^>]*>(.*?)</summary>\s*<p[^>]*>(.*?)</p>\s*</details>' % lang ,
6680 re .S
6781 )
6882 return [(clean_faq_text (q ), clean_faq_text (a )) for q , a in pattern .findall (html )]
6983
84+
7085def faq_mainentity_json (pairs ):
7186 entries = []
7287 for q , a in pairs :
@@ -79,77 +94,190 @@ def faq_mainentity_json(pairs):
7994 )
8095 return '"mainEntity": [\n ' + ',\n ' .join (entries ) + '\n ]'
8196
82- EN_HOWTO_STEPS = [
83- "Answer 10 questions about your behavior." ,
84- "Receive your Chinese zodiac sign and archetype." ,
85- "Choose a partner's sign and context (love/friendship/work)." ,
86- "See compatibility percentage and personalized insights." ,
87- ]
8897
89- with io .open ("index.html" , "r" , encoding = "utf-8" ) as f :
90- template = f .read ()
98+ VOID_TAGS = {"area" , "base" , "br" , "col" , "embed" , "hr" , "img" , "input" ,
99+ "link" , "meta" , "param" , "source" , "track" , "wbr" }
100+
101+
102+ class LangFilter (HTMLParser ):
103+ """Strips every element carrying a data-lang attribute that doesn't match
104+ target_lang (and its whole subtree), so the output is genuinely single-language
105+ instead of all 4 languages hidden/shown via CSS+JS."""
106+
107+ def __init__ (self , target_lang ):
108+ super ().__init__ (convert_charrefs = False )
109+ self .target_lang = target_lang
110+ self .out = []
111+ self .skip_stack = []
112+ self .depth = 0
113+
114+ def _skipping (self ):
115+ return len (self .skip_stack ) > 0
116+
117+ def _render_starttag (self , tag , attrs , self_close ):
118+ parts = [f"<{ tag } " ]
119+ for k , v in attrs :
120+ if k == "data-lang" :
121+ continue
122+ parts .append (f" { k } " if v is None else f' { k } ="{ v } "' )
123+ parts .append (" />" if self_close else ">" )
124+ self .out .append ("" .join (parts ))
125+
126+ def handle_starttag (self , tag , attrs ):
127+ lang = dict (attrs ).get ("data-lang" )
128+ if tag in VOID_TAGS :
129+ if self ._skipping ():
130+ return
131+ if lang is not None and lang != self .target_lang :
132+ return
133+ self ._render_starttag (tag , attrs , self_close = False )
134+ return
135+ self .depth += 1
136+ if self ._skipping ():
137+ return
138+ if lang is not None and lang != self .target_lang :
139+ self .skip_stack .append (self .depth )
140+ return
141+ self ._render_starttag (tag , attrs , self_close = False )
142+
143+ def handle_startendtag (self , tag , attrs ):
144+ if self ._skipping ():
145+ return
146+ lang = dict (attrs ).get ("data-lang" )
147+ if lang is not None and lang != self .target_lang :
148+ return
149+ self ._render_starttag (tag , attrs , self_close = True )
150+
151+ def handle_endtag (self , tag ):
152+ if self .skip_stack and self .skip_stack [- 1 ] == self .depth :
153+ self .skip_stack .pop ()
154+ self .depth -= 1
155+ return
156+ self .depth -= 1
157+ if self ._skipping ():
158+ return
159+ self .out .append (f"</{ tag } >" )
91160
92- import os
161+ def handle_data (self , data ):
162+ if not self ._skipping ():
163+ self .out .append (data )
93164
94- # Keep index.html's own JSON-LD FAQPage in sync with its visible <details data-lang="en"> blocks,
95- # since index.html is the source template and never gets "built" from anything else.
96- _en_pairs = extract_faq (template , "en" )
97- _en_faq_block = faq_mainentity_json (_en_pairs )
98- _resynced = re .sub (r'"mainEntity": \[.*?\n \]' , lambda m : _en_faq_block , template , count = 1 , flags = re .S )
99- if _resynced != template :
100- template = _resynced
101- with io .open ("index.html" , "w" , encoding = "utf-8" , newline = "\n " ) as f :
102- f .write (template )
103- print ("Resynced index.html JSON-LD FAQ to" , len (_en_pairs ), "entries" )
165+ def handle_entityref (self , name ):
166+ if not self ._skipping ():
167+ self .out .append (f"&{ name } ;" )
104168
105- for lang , t in LANGS .items ():
169+ def handle_charref (self , name ):
170+ if not self ._skipping ():
171+ self .out .append (f"&#{ name } ;" )
172+
173+ def handle_comment (self , data ):
174+ if not self ._skipping ():
175+ self .out .append (f"<!--{ data } -->" )
176+
177+ def handle_decl (self , decl ):
178+ if not self ._skipping ():
179+ self .out .append (f"<!{ decl } >" )
180+
181+ def filtered (self , html ):
182+ self .feed (html )
183+ self .close ()
184+ return "" .join (self .out )
185+
186+
187+ with io .open ("template.html" , "r" , encoding = "utf-8" ) as f :
188+ template = f .read ()
189+
190+ for lang in ALL_LANGS :
106191 html = template
192+ is_en = lang == "en"
193+ t = LANGS .get (lang )
107194
108195 html = html .replace ('<html lang="en">' , '<html lang="%s" data-forced-lang="%s">' % (lang , lang ), 1 )
109196
110- html = re .sub (r'(<title>)[^<]*(</title>)' , lambda m : m .group (1 ) + t ["title" ] + m .group (2 ), html , count = 1 )
111- html = re .sub (r'(<meta name="description" content=")[^"]*(")' , lambda m : m .group (1 ) + t ["desc" ] + m .group (2 ), html , count = 1 )
112- html = re .sub (r'(<meta name="keywords" content=")[^"]*(")' , lambda m : m .group (1 ) + t ["keywords" ] + m .group (2 ), html , count = 1 )
113- html = re .sub (r'(<link rel="canonical" href=")[^"]*(")' , lambda m : m .group (1 ) + BASE + "/" + lang + "/" + m .group (2 ), html , count = 1 )
114- html = re .sub (r'(<meta property="og:title" content=")[^"]*(")' , lambda m : m .group (1 ) + t ["og_title" ] + m .group (2 ), html , count = 1 )
115- html = re .sub (r'(<meta property="og:description" content=")[^"]*(")' , lambda m : m .group (1 ) + t ["og_desc" ] + m .group (2 ), html , count = 1 )
116- html = re .sub (r'(<meta property="og:url" content=")[^"]*(")' , lambda m : m .group (1 ) + BASE + "/" + lang + "/" + m .group (2 ), html , count = 1 )
117- html = re .sub (r'(<meta property="og:locale" content=")[^"]*(")' , lambda m : m .group (1 ) + t ["locale" ] + m .group (2 ), html , count = 1 )
118-
119- # localize JSON-LD: Product description, FAQPage Q&A, HowTo name+steps
120- html = html .replace (
121- '"description": "Free online test to discover your Chinese zodiac sign, behavioral archetype, and compatibility. No registration required. Compare Western and Vedic astrology. Based on Swiss Ephemeris calculations.",' ,
122- '"description": "%s",' % t ["product_desc" ],
123- 1
124- )
197+ if not is_en :
198+ html = re .sub (r'(<title>)[^<]*(</title>)' , lambda m : m .group (1 ) + t ["title" ] + m .group (2 ), html , count = 1 )
199+ html = re .sub (r'(<meta name="description" content=")[^"]*(")' , lambda m : m .group (1 ) + t ["desc" ] + m .group (2 ), html , count = 1 )
200+ html = re .sub (r'(<meta name="keywords" content=")[^"]*(")' , lambda m : m .group (1 ) + t ["keywords" ] + m .group (2 ), html , count = 1 )
201+ html = re .sub (r'(<link rel="canonical" href=")[^"]*(")' , lambda m : m .group (1 ) + BASE + "/" + lang + "/" + m .group (2 ), html , count = 1 )
202+ html = re .sub (r'(<meta property="og:title" content=")[^"]*(")' , lambda m : m .group (1 ) + t ["og_title" ] + m .group (2 ), html , count = 1 )
203+ html = re .sub (r'(<meta property="og:description" content=")[^"]*(")' , lambda m : m .group (1 ) + t ["og_desc" ] + m .group (2 ), html , count = 1 )
204+ html = re .sub (r'(<meta property="og:url" content=")[^"]*(")' , lambda m : m .group (1 ) + BASE + "/" + lang + "/" + m .group (2 ), html , count = 1 )
205+ html = re .sub (r'(<meta property="og:locale" content=")[^"]*(")' , lambda m : m .group (1 ) + t ["locale" ] + m .group (2 ), html , count = 1 )
206+
207+ html = html .replace (
208+ '"description": "%s",' % EN_PRODUCT_DESC ,
209+ '"description": "%s",' % t ["product_desc" ],
210+ 1
211+ )
212+ html = html .replace ('"name": "%s",' % EN_HOWTO_NAME , '"name": "%s",' % t ["howto_name" ], 1 )
213+ for en_s , loc_s in zip (EN_HOWTO_STEPS , t ["howto_steps" ]):
214+ html = html .replace ('"text": "%s" }' % en_s , '"text": "%s" }' % loc_s , 1 )
215+
216+ html = html .replace ('<body class="lang-en">' , '<body class="lang-%s">' % lang , 1 )
217+
218+ # relative asset paths need ../ prefix from a subdirectory
219+ html = html .replace ('href="favicon.ico"' , 'href="../favicon.ico"' )
220+ html = html .replace ('href="apple-touch-icon.png"' , 'href="../apple-touch-icon.png"' )
221+ html = html .replace ('src="cover.jpg"' , 'src="../cover.jpg"' )
222+ html = html .replace ('srcset="cover.webp"' , 'srcset="../cover.webp"' )
223+ # terms.html/privacy.html exist in every language folder now (no ../ prefix needed)
224+ html = html .replace ('href="icon-192.png"' , 'href="../icon-192.png"' )
225+ html = html .replace ('href="manifest.json"' , 'href="../manifest.json"' )
226+ html = html .replace ('href="assets/css/styles.css"' , 'href="../assets/css/styles.css"' )
227+ html = html .replace ('src="assets/js/main.js"' , 'src="../assets/js/main.js"' )
228+
229+ # FAQPage JSON-LD must reflect only this language's Q&A
125230 faq_pairs = extract_faq (template , lang )
126231 new_faq_block = faq_mainentity_json (faq_pairs )
127- html = re .sub (
128- r'"mainEntity": \[.*?\n \]' ,
129- lambda m : new_faq_block ,
130- html , count = 1 , flags = re .S
131- )
132- html = html .replace ('"name": "How to use AstroScan",' , '"name": "%s",' % t ["howto_name" ], 1 )
133- for en_s , loc_s in zip (EN_HOWTO_STEPS , t ["howto_steps" ]):
134- html = html .replace ('"text": "%s" }' % en_s , '"text": "%s" }' % loc_s , 1 )
135-
136- html = html .replace ('<body class="lang-en">' , '<body class="lang-%s">' % lang , 1 )
137-
138- # relative asset paths need ../ prefix from a subdirectory
139- html = html .replace ('href="favicon.ico"' , 'href="../favicon.ico"' )
140- html = html .replace ('href="apple-touch-icon.png"' , 'href="../apple-touch-icon.png"' )
141- html = html .replace ('src="cover.jpg"' , 'src="../cover.jpg"' )
142- html = html .replace ('srcset="cover.webp"' , 'srcset="../cover.webp"' )
143- html = html .replace ('href="terms.html"' , 'href="../terms.html"' )
144- html = html .replace ('href="privacy.html"' , 'href="../privacy.html"' )
145- html = html .replace ('href="icon-192.png"' , 'href="../icon-192.png"' )
146- html = html .replace ('href="manifest.json"' , 'href="../manifest.json"' )
147- html = html .replace ('href="assets/css/styles.css"' , 'href="../assets/css/styles.css"' )
148- html = html .replace ('src="assets/js/main.js"' , 'src="../assets/js/main.js"' )
149-
150- os .makedirs (lang , exist_ok = True )
151- with io .open (os .path .join (lang , "index.html" ), "w" , encoding = "utf-8" , newline = "\n " ) as f :
232+ html = re .sub (r'"mainEntity": \[.*?\n \]' , lambda m : new_faq_block , html , count = 1 , flags = re .S )
233+
234+ # Strip every other language's markup so the deployed page is single-language
235+ html = LangFilter (lang ).filtered (html )
236+
237+ out_path = "index.html" if is_en else os .path .join (lang , "index.html" )
238+ if not is_en :
239+ os .makedirs (lang , exist_ok = True )
240+ with io .open (out_path , "w" , encoding = "utf-8" , newline = "\n " ) as f :
152241 f .write (html )
153- print ("OK" , lang + "/index.html" , len (html ), "bytes" )
242+ print ("OK" , out_path , len (html ), "bytes" )
243+
244+ print ("Done:" , ", " .join (ALL_LANGS ))
245+
246+ # --- Legal pages (privacy.html, terms.html): same single-language-per-page approach ---
247+ LANG_BAR_NAMES = {"en" : "English" , "ru" : "Русский" , "es" : "Español" , "hi" : "हिन्दी" }
248+
249+ for page in ["privacy" , "terms" ]:
250+ with io .open ("%s_template.html" % page , "r" , encoding = "utf-8" ) as f :
251+ legal_template = f .read ()
252+
253+ lang_bar_re = re .compile (r'<div class="lang-bar">.*?</div>' , re .S )
254+ script_re = re .compile (r'\s*<script>\s*function setPLang.*?</script>' , re .S )
255+
256+ for lang in ALL_LANGS :
257+ html = legal_template
258+ is_en = lang == "en"
259+ canon = BASE + ("/%s.html" % page if is_en else "/%s/%s.html" % (lang , page ))
260+
261+ html = html .replace ('<html lang="en">' , '<html lang="%s">' % lang , 1 )
262+ html = re .sub (r'(<link rel="canonical" href=")[^"]*(")' , lambda m : m .group (1 ) + canon + m .group (2 ), html , count = 1 )
263+ html = html .replace ('<body class="lang-en">' , '<body class="lang-%s">' % lang , 1 )
264+
265+ lang_bar_links = "\n " .join (
266+ ' <a href="%s">%s</a>' % (
267+ BASE + ("/%s.html" % page if l == "en" else "/%s/%s.html" % (l , page )),
268+ LANG_BAR_NAMES [l ]
269+ ) for l in ALL_LANGS
270+ )
271+ html = lang_bar_re .sub ('<div class="lang-bar">\n %s\n </div>' % lang_bar_links , html , count = 1 )
272+ html = script_re .sub ('' , html , count = 1 )
273+
274+ html = LangFilter (lang ).filtered (html )
275+
276+ out_path = "%s.html" % page if is_en else os .path .join (lang , "%s.html" % page )
277+ if not is_en :
278+ os .makedirs (lang , exist_ok = True )
279+ with io .open (out_path , "w" , encoding = "utf-8" , newline = "\n " ) as f :
280+ f .write (html )
281+ print ("OK" , out_path , len (html ), "bytes" )
154282
155- print ("Done:" , ", " . join ( LANGS . keys ()) )
283+ print ("Done: privacy, terms" )
0 commit comments