§
    ³Âjƒ;  ã                   ó¢  — d Z ddlZddlmc mZ ddlZddlZddl	Z	ddl
Z
ddlZddlmZ  ee¦  «                             ¦   «         j        j        j        Zedz  dz  Zedz  dz  dz  Zedz  dz  d	z  Zedz  dz  d
z  Zedz  dz  dz  Zedz  dz  dz  Zedz  dz  dz  Zedz  Z G d„ dej        ¦  «        Zedk    r ej        ¦   «          dS dS )a  
Unit tests for FreeExile Unified WebApp Localization & Multi-Language Synchronization Engine.
Verifies complete 9-language coverage, dynamic canvas overhead nameplate localization,
interaction prompt translation, and reactive hot-swapping between Vietnamese and English.
é    N)ÚPathÚclientÚwebappÚjsÚdatazi18n.jszi18n_catalog.jszchat_i18n_catalog.jsÚenginezentity_renderer.jsÚuizdialogue_npc.jsztooltips.jsz
index.htmlc                   ó„   — e Zd Zed„ ¦   «         Zd„ Zd„ Zd„ Zd„ Zd„ Z	d„ Z
d„ Zd	„ Zd
„ Zd„ Zd„ Zd„ Zd„ Zd„ Zd„ Zd„ ZdS )ÚTestWebAppLocalizationEnginec                 óî  — t          t          ¦  «                             dd¦  «        }d|› d�}t          j        dd|gddd¬	¦  «        }t          j        |j        ¦  «        | _        t           
                    ¦   «         rmt                               ¦   «                              ¦   «         }d
|› d�}t          j        ddd|gddd¬	¦  «        }t          j        |j        ¦  «        | _        dS i | _        dS )z[Load and evaluate I18N_CATALOG and CHAT_I18N_CATALOG via node for deep semantic assertions.ú\ú/zI
        const fs = require('fs');
        const code = fs.readFileSync('z¤', 'utf-8');
        const fakeWindow = {};
        new Function('window', code)(fakeWindow);
        console.log(JSON.stringify(fakeWindow.I18N_CATALOG));
        Únodez-eTúutf-8)Úcapture_outputÚencodingÚcheckz
            import('zm').then(m => {
                console.log(JSON.stringify(m.CHAT_I18N_CATALOG));
            });
            z--input-type=moduleN)ÚstrÚI18N_CATALOG_JSÚreplaceÚ
subprocessÚrunÚjsonÚloadsÚstdoutÚcatalogÚCHAT_I18N_CATALOG_JSÚexistsÚresolveÚas_uriÚchat_catalog)ÚclsÚescaped_pathÚnode_scriptÚresÚchat_urlÚchat_node_scriptÚres_chats          úCC:\Projects\FreeExile\tests\unit\test_webapp_localization_engine.pyÚ
setUpClassz'TestWebAppLocalizationEngine.setUpClass   s  € õ �?Ñ+Ô+×3Ò3°D¸#Ñ>Ô>ˆðà'3ðð ð ˆõ Œn˜f d¨KÐ8ÈÐX_ÐgkÐlÑlÔlˆÝ”j ¤Ñ,Ô,ˆŒå×&Ò&Ñ(Ô(ð 
	"Ý+×3Ò3Ñ5Ô5×<Ò<Ñ>Ô>ˆHð Øð ð  ð  Ðõ
 "”~ vÐ/DÀdÐL\Ð&]Ðnrð  ~Eð  MQð  Rñ  Rô  RˆHÝ#œz¨(¬/Ñ:Ô:ˆCÔÐÐà!ˆCÔÐÐó    c                 ó  — |                       t                               ¦   «         d¦  «         |                       t                               ¦   «         d¦  «         t	          t
          dd¬¦  «        5 }|                     ¦   «         }ddd¦  «         n# 1 swxY w Y   |                      d|d¦  «         |                      d	|d
¦  «         |                     d¦  «        }|                     d	¦  «        }|  	                    ||d¦  «         dS )zVi18n_catalog.js and i18n.js must exist and be included in index.html in correct order.z&i18n_catalog.js must exist in js/data/zi18n.js must exist in js/data/Úrr   ©r   Nzjs/data/i18n_catalog.jsz&index.html must import i18n_catalog.jszjs/data/i18n.jszindex.html must import i18n.jsz/i18n_catalog.js must be imported before i18n.js)
Ú
assertTruer   r   ÚI18N_JSÚopenÚ
INDEX_HTMLÚreadÚassertInÚindexÚ
assertLess)ÚselfÚfÚhtmlÚcatalog_idxÚi18n_idxs        r)   Ú*test_i18n_files_exist_and_imported_in_htmlzGTestWebAppLocalizationEngine.test_i18n_files_exist_and_imported_in_html4   s)  € à�Š�×.Ò.Ñ0Ô0Ð2ZÑ[Ô[Ð[Ø�Š�ŸšÑ(Ô(Ð*JÑKÔKÐKå•*˜c¨GÐ4Ñ4Ô4ð 	¸Ø—6’6‘8”8ˆDð	ð 	ð 	ñ 	ô 	ð 	ð 	ð 	ð 	ð 	ð 	øøøð 	ð 	ð 	ð 	ð 	�ŠÐ/°Ð7_Ñ`Ô`Ð`Ø�ŠÐ'¨Ð/OÑPÔPÐPà—j’jÐ!:Ñ;Ô;ˆØ—:’:Ð/Ñ0Ô0ˆØ�Š˜ XÐ/`ÑaÔaÐaÐaÐas   Á2BÂBÂBc                 óÜ   — t          t          dd¬¦  «        5 }|                     ¦   «         }ddd¦  «         n# 1 swxY w Y   g d¢}|D ]!}|                      |d|› d�d|› d	�¦  «         Œ"dS )
zJI18N_CATALOG must define dictionaries for all 9 required global languages.r-   r   r.   N©	ÚviÚenÚzhÚjaÚkoÚthÚdeÚruÚesz['\"]?z['\"]?\s*:\s*\{úLocale 'ú' must exist in I18N_CATALOG)r1   r   r3   ÚassertRegex)r7   r8   Úcatalog_codeÚlocalesÚlocs        r)   Ú"test_all_nine_languages_in_catalogz?TestWebAppLocalizationEngine.test_all_nine_languages_in_catalogC   sÕ   € å•/ 3°Ð9Ñ9Ô9ð 	$¸QØŸ6š6™8œ8ˆLð	$ð 	$ð 	$ñ 	$ô 	$ð 	$ð 	$ð 	$ð 	$ð 	$ð 	$øøøð 	$ð 	$ð 	$ð 	$ð IÐHÐHˆØð 	zð 	zˆCØ×Ò˜\Ð+J°SÐ+JÐ+JÐ+JÐLxÐWZÐLxÐLxÐLxÑyÔyÐyÐyð	zð 	zó   ˜9¹=Á =c                 óÔ   — t          t          dd¬¦  «        5 }|                     ¦   «         }ddd¦  «         n# 1 swxY w Y   g d¢}|D ]}|                      ||d|› d�¦  «         ŒdS )zSEssential NPC names, titles, services, and character names must exist in EN and VI.r-   r   r.   N)Ú	char_nameÚnpc_van_gioi_thuong_nhan_nameÚnpc_van_gioi_thuong_nhan_titleÚ npc_van_gioi_thuong_nhan_serviceÚnpc_thiet_tuong_au_da_tu_nameÚnpc_thiet_tuong_au_da_tu_titleÚ npc_thiet_tuong_au_da_tu_serviceÚnpc_chan_vo_truyen_thua_nameÚnpc_chan_vo_truyen_thua_titleÚnpc_chan_vo_truyen_thua_serviceÚnpc_doc_co_tan_kiem_nameÚnpc_doc_co_tan_kiem_titleÚnpc_doc_co_tan_kiem_serviceÚnpc_sa_mac_du_than_nameÚnpc_sa_mac_du_than_titleÚnpc_sa_mac_du_than_servicezTranslation key 'rI   ©r1   r   r3   r4   )r7   r8   rK   Úrequired_keysÚkeys        r)   Ú*test_npc_and_character_translations_parityzGTestWebAppLocalizationEngine.test_npc_and_character_translations_parityL   sË   € å•/ 3°Ð9Ñ9Ô9ð 	$¸QØŸ6š6™8œ8ˆLð	$ð 	$ð 	$ñ 	$ô 	$ð 	$ð 	$ð 	$ð 	$ð 	$ð 	$øøøð 	$ð 	$ð 	$ð 	$ð
ð 
ð 
ˆð& !ð 	dð 	dˆCØ�MŠM˜#˜|Ð-bÀÐ-bÐ-bÐ-bÑcÔcÐcÐcð	dð 	drO   c                 óê   — t          t          dd¬¦  «        5 }|                     ¦   «         }ddd¦  «         n# 1 swxY w Y   |                      d|d¦  «         |                      d|v d¦  «         dS )	uW   Canvas entity renderer must NOT hardcode 'Káº» Bá»‹ LÆ°u Ä�Ã y' but query I18N instead.r-   r   r.   Nu#   ctx.fillText("Káº» Bá»‹ LÆ°u Ä�Ã y"u;   entity_renderer.js must not hardcode 'Káº» Bá»‹ LÆ°u Ä�Ã y'ÚI18Nz8entity_renderer.js must query I18N for entity nameplates)r1   ÚENTITY_RENDERER_JSr3   ÚassertNotInr/   ©r7   r8   Úcodes      r)   Ú8test_entity_renderer_no_hardcoded_vietnamese_player_namezUTestWebAppLocalizationEngine.test_entity_renderer_no_hardcoded_vietnamese_player_nameg   s¿   € åÕ$ c°GÐ<Ñ<Ô<ð 	ÀØ—6’6‘8”8ˆDð	ð 	ð 	ñ 	ô 	ð 	ð 	ð 	ð 	ð 	ð 	øøøð 	ð 	ð 	ð 	ð 	×ÒÐ>Àð  GDñ  	Eô  	Eð  	Eà�Š˜ $˜Ð(bÑcÔcÐcÐcÐcrO   c                 óè   — t          t          dd¬¦  «        5 }|                     ¦   «         }ddd¦  «         n# 1 swxY w Y   |                      d|d¦  «         |                      d|d¦  «         dS )	zENPC floating nameplates and monster names must be localized via I18N.r-   r   r.   NÚ
getNpcNamez+entity_renderer.js must use I18N.getNpcNameÚgetNpcTitlez,entity_renderer.js must use I18N.getNpcTitle)r1   rg   r3   r4   ri   s      r)   Ú9test_entity_renderer_localized_npc_and_monster_nameplateszVTestWebAppLocalizationEngine.test_entity_renderer_localized_npc_and_monster_nameplatesq   s­   € åÕ$ c°GÐ<Ñ<Ô<ð 	ÀØ—6’6‘8”8ˆDð	ð 	ð 	ñ 	ô 	ð 	ð 	ð 	ð 	ð 	ð 	øøøð 	ð 	ð 	ð 	ð 	�Š�l DÐ*WÑXÔXÐXØ�Š�m TÐ+YÑZÔZÐZÐZÐZrO   c                 ó¼   — t          t          dd¬¦  «        5 }|                     ¦   «         }ddd¦  «         n# 1 swxY w Y   |                      d|v d¦  «         dS )zJNearby NPC interaction titles must dynamically adapt to selected language.r-   r   r.   Nrf   z(dialogue_npc.js must integrate with I18N)r1   ÚDIALOGUE_NPC_JSr3   r/   ri   s      r)   Ú3test_dialogue_npc_localized_interaction_and_promptszPTestWebAppLocalizationEngine.test_dialogue_npc_localized_interaction_and_promptsy   s—   € å•/ 3°Ð9Ñ9Ô9ð 	¸QØ—6’6‘8”8ˆDð	ð 	ð 	ñ 	ô 	ð 	ð 	ð 	ð 	ð 	ð 	øøøð 	ð 	ð 	ð 	ð 	�Š˜ $˜Ð(RÑSÔSÐSÐSÐSrO   c                 óÌ   — t          t          dd¬¦  «        5 }|                     ¦   «         }ddd¦  «         n# 1 swxY w Y   |                      d|v pd|v pd|v d¦  «         dS )	zBTooltips engine must look up localized title and description keys.r-   r   r.   NÚtooltipTitleKeyrf   z	data-i18nz/tooltips.js must support localized tooltip keys)r1   ÚTOOLTIPS_JSr3   r/   ri   s      r)   Ú'test_tooltips_support_localization_keyszDTestWebAppLocalizationEngine.test_tooltips_support_localization_keys�   s¶   € å•+˜s¨WÐ5Ñ5Ô5ð 	¸Ø—6’6‘8”8ˆDð	ð 	ð 	ñ 	ô 	ð 	ð 	ð 	ð 	ð 	ð 	øøøð 	ð 	ð 	ð 	ð 	�ŠØ Ð%ÐN¨°4¨ÐN¸;È$Ð;NØ=ñ	
ô 	
ð 	
ð 	
ð 	
rO   c                 ó^  — t          t          dd¬¦  «        5 }|                     ¦   «         }ddd¦  «         n# 1 swxY w Y   |                      d|¦  «         |                      d|v od|v d¦  «         |                      d	|v pd
|v pd|v d¦  «         |                      d|d¦  «         dS )zPDialogue modal must resolve node text, choice text, and prompt buttons via I18N.r-   r   r.   NÚdialogueContentzI18N.tÚ	dialogue_zCdialogue_npc.js must query I18N.t for dialogue body text with a keyÚdialogue_choice_z
ch.textKeyzI18N.t(z2dialogue_npc.js must query I18N.t for choice textsÚdialogue_selectuH   dialogue_npc.js must localize 'Chá»�n â†µ' button with 'dialogue_select')r1   rq   r3   r4   r/   ri   s      r)   Ú(test_dialogue_npc_multilingual_renderingzETestWebAppLocalizationEngine.test_dialogue_npc_multilingual_rendering‹   s  € å•/ 3°Ð9Ñ9Ô9ð 	¸QØ—6’6‘8”8ˆDð	ð 	ð 	ñ 	ô 	ð 	ð 	ð 	ð 	ð 	ð 	øøøð 	ð 	ð 	ð 	ð 	�ŠÐ'¨Ñ.Ô.Ð.à�ŠØ˜ÐÐ4 °Ð!4ØQñ	
ô 	
ð 	
ð
 	�ŠØ $Ð&ÐS¨,¸$Ð*>ÐSÀ)ÈtÐBSØ@ñ	
ô 	
ð 	
ð
 	�ŠÐ'¨Ð/yÑzÔzÐzÐzÐzrO   c                 óx  — t          t          dd¬¦  «        5 }|                     ¦   «         }ddd¦  «         n# 1 swxY w Y   t          dz  }|                     ¦   «         r4|                     d¦  «        D ]}|d|                     d¬¦  «        z   z  }Œg d¢}|D ]}|                      ||d	|› d
�¦  «         ŒdS )zQImportant modal titles and sub-tabs in index.html must have data-i18n attributes.r-   r   r.   NÚ	templatesz*.htmlú
)z data-i18n="modal_meridian_title"zdata-i18n="modal_vault_title"zdata-i18n="vault_tab_market"zdata-i18n="vault_tab_guild"zdata-i18n="vault_tab_stats"zdata-i18n="shop_tab_coins"z%index.html or templates must include z for automatic localization)r1   r2   r3   Ú
WEBAPP_DIRÚis_dirÚglobÚ	read_textr4   )r7   r8   r9   Útemplates_dirÚtplÚexpected_i18n_keysrc   s          r)   Ú+test_modal_headers_and_tabs_i18n_attributeszHTestWebAppLocalizationEngine.test_modal_headers_and_tabs_i18n_attributesž   s&  € å•*˜c¨GÐ4Ñ4Ô4ð 	¸Ø—6’6‘8”8ˆDð	ð 	ð 	ñ 	ô 	ð 	ð 	ð 	ð 	ð 	ð 	øøøð 	ð 	ð 	ð 	å" [Ñ0ˆØ×ÒÑ!Ô!ð 	?Ø$×)Ò)¨(Ñ3Ô3ð ?ð ?�Ø˜˜sŸ}š}°g˜}Ñ>Ô>Ñ>Ñ>��ð
ð 
ð 
Ðð &ð 	oð 	oˆCØ�MŠM˜#˜tÐ%mÈSÐ%mÐ%mÐ%mÑnÔnÐnÐnð	oð 	orO   c                 óÔ   — t          t          dd¬¦  «        5 }|                     ¦   «         }ddd¦  «         n# 1 swxY w Y   g d¢}|D ]}|                      ||d|› d�¦  «         ŒdS )z_Catalog must contain essential keys for dialogue choices, notices, and modal tabs in EN and VI.r-   r   r.   N)r{   Úvault_tab_marketÚvault_tab_guildÚvault_tab_statsÚshop_tab_coinsÚnotice_forge_activeÚnotice_quest_doc_coúKey 'rI   ra   )r7   r8   r   Úkeys_to_verifyÚks        r)   Ú$test_dialogue_and_ui_keys_in_catalogzATestWebAppLocalizationEngine.test_dialogue_and_ui_keys_in_catalog²   sË   € å•/ 3°Ð9Ñ9Ô9ð 	¸QØ—f’f‘h”hˆGð	ð 	ð 	ñ 	ô 	ð 	ð 	ð 	ð 	ð 	ð 	øøøð 	ð 	ð 	ð 	ð
ð 
ð 
ˆð  ð 	Oð 	OˆAØ�MŠM˜!˜WÐ&M¨aÐ&MÐ&MÐ&MÑNÔNÐNÐNð	Oð 	OrO   c                 óŠ  — g d¢}t          | j        d                              ¦   «         ¦  «        }|                      t	          |¦  «        ddt	          |¦  «        › �¦  «         |D �]Z}|                      || j        d|› d�¦  «         t          | j        |                              ¦   «         ¦  «        }||z
  }|                      t	          |¦  «        dd|› dt	          |¦  «        › d	|› �¦  «         ||z
  }|                      t	          |¦  «        dd|› d
|› �¦  «         |                      t	          |¦  «        d¦  «         | j        |                              ¦   «         D ]Z\  }}|                      t          |t          ¦  «        o$t	          | 
                    ¦   «         ¦  «        dk    d|› d|› d�¦  «         Œ[�Œ\dS )zd100% key parity across all 9 languages (all 143 keys present in vi, en, zh, ja, ko, th, de, ru, es).r>   r?   é�   z+Expected 143 keys in master catalog, found rH   z' missing from I18N_CATALOGr   z' is missing z keys: z' has unexpected extra keys: r�   z' in [z] must be non-empty stringN)Úsetr   ÚkeysÚassertEqualÚlenr4   Úitemsr/   Ú
isinstancer   Ústrip)	r7   rL   Úmaster_keysrM   Úloc_keysÚmissingÚextrar‘   Úvals	            r)   Ú,test_full_nine_language_key_parity_and_countzITestWebAppLocalizationEngine.test_full_nine_language_key_parity_and_countÃ   sê  € àHÐHÐHˆÝ˜$œ, tÔ,×1Ò1Ñ3Ô3Ñ4Ô4ˆØ×Ò�˜[Ñ)Ô)¨3Ð0pÕ^aÐbmÑ^nÔ^nÐ0pÐ0pÑqÔqÐqàð 		Añ 		AˆCØ�MŠM˜#˜tœ|Ð-X¸Ð-XÐ-XÐ-XÑYÔYÐYÝ˜4œ<¨Ô,×1Ò1Ñ3Ô3Ñ4Ô4ˆHØ! HÑ,ˆGØ×Ò�S ™\œ\¨1Ð.i¸Ð.iÐ.iÍ3ÈwÉ<Ì<Ð.iÐ.iÐ`gÐ.iÐ.iÑjÔjÐjØ˜{Ñ*ˆEØ×Ò�S ™ZœZ¨Ð,`°sÐ,`Ð,`ÐY^Ð,`Ð,`ÑaÔaÐaØ×Ò�S ™]œ]¨CÑ0Ô0Ð0Øœ, sÔ+×1Ò1Ñ3Ô3ð Að A‘��3Ø—’¥
¨3µÑ 4Ô 4Ð M½¸S¿YºY¹[¼[Ñ9IÔ9IÈAÒ9MÐOÐWXÐOÐOÐ`cÐOÐOÐOñ  Aô  Að  Að  AñAð		Að 		Ar+   c                 ó   ‡— d}h d£}t          j        d¦  «        }| j                             ¦   «         D �]—\  }}|                     ¦   «         D �]{\  Š}t	          ˆfd„|D ¦   «         ¦  «        p‰|v }|�rS‰                     d¦  «        �s=‰                     d¦  «        �s'|                     d|¦  «                             ¦   «         }|dv rC|                     ¦   «         }	|  	                    t          |	¦  «        d	d
|› d‰› d|› d�¦  «         ŒÃ|dk    r<t          d„ |D ¦   «         ¦  «        }
|  	                    |
dd
|› d‰› d|› d�¦  «         �Œ|dk    r<t          d„ |D ¦   «         ¦  «        }
|  	                    |
dd
|› d‰› d|› d�¦  «         �ŒG|dk    r.|  	                    t          |¦  «        dd
|› d‰› d|› d�¦  «         �Œ}�Œ™dS )zX1-2 word microcopy assertion on action labels, tabs, and buttons across all 9 languages.)Úagent_stance_>   ÚdodgeÚbuyoutr{   Úagent_btn_revokeÚdialogue_btn_closeÚdialogue_btn_leavez1[\U00010000-\U0010ffff\u2600-\u27ff\ufe00-\ufe0f]c              3   óB   •K  — | ]}‰                      |¦  «        V — Œd S ©N©Ú
startswith©Ú.0Úprc   s     €r)   ú	<genexpr>zjTestWebAppLocalizationEngine.test_microcopy_brevity_one_to_two_words_all_nine_languages.<locals>.<genexpr>ß   s/   øè è € ÐBÐB¨a˜Ÿš qÑ)Ô)ÐBÐBÐBÐBÐBÐBr+   Ú_descÚ	_subtitleÚ )r?   r@   rE   rF   rG   rC   é   zUI Label in [ú] 'z' exceeds 2 words: 'ú'rA   c                 ó:   — g | ]}|                      ¦   «         °|‘ŒS © ©Úisspace©r®   Úcs     r)   ú
<listcomp>zkTestWebAppLocalizationEngine.test_microcopy_brevity_one_to_two_words_all_nine_languages.<locals>.<listcomp>è   ó%   € Ð$IÐ$IÐ$I¨1¸Q¿YºY¹[¼[Ð$I QÐ$IÐ$IÐ$Ir+   é   z' exceeds 4 characters: 'rB   c                 ó:   — g | ]}|                      ¦   «         °|‘ŒS r¸   r¹   r»   s     r)   r½   zkTestWebAppLocalizationEngine.test_microcopy_brevity_one_to_two_words_all_nine_languages.<locals>.<listcomp>ë   r¾   r+   é   z' exceeds 6 characters: 'rD   é   z' exceeds 12 characters: 'N)ÚreÚcompiler   r™   ÚanyÚendswithÚsubr›   ÚsplitÚassertLessEqualr˜   )r7   Úshort_prefixesÚexplicit_shortÚemoji_cleanerrM   Údict_locr    Úis_shortÚcleanÚwordsÚcharsrc   s              @r)   Ú:test_microcopy_brevity_one_to_two_words_all_nine_languageszWTestWebAppLocalizationEngine.test_microcopy_brevity_one_to_two_words_all_nine_languagesÔ   s]  ø€ à+ˆð
ð 
ð 
ˆõ œ
Ð#WÑXÔXˆà!œ\×/Ò/Ñ1Ô1ð 	|ñ 	|‰MˆC�Ø$ŸNšNÑ,Ô,ð |ñ |‘��SåÐBÐBÐBÐB°>ÐBÑBÔBÑBÔBð *Ø˜>Ð)ð ð ñ | C§L¢L°Ñ$9Ô$9ñ |À#Ç,Â,È{ÑB[ÔB[ñ |Ø)×-Ò-¨b°#Ñ6Ô6×<Ò<Ñ>Ô>�EØÐBÐBÐBØ %§¢¡¤˜Ø×,Ò,­S°©Z¬Z¸Ð<sÈCÐ<sÐ<sÐTWÐ<sÐ<sÐmpÐ<sÐ<sÐ<sÑtÔtÐtÐtØ š˜Ý #Ð$IÐ$I°Ð$IÑ$IÔ$IÑ JÔ J˜Ø×,Ò,¨U°AÐ7sÀsÐ7sÐ7sÈsÐ7sÐ7sÐmpÐ7sÐ7sÐ7sÑtÔtÐtÑtØ š˜Ý #Ð$IÐ$I°Ð$IÑ$IÔ$IÑ JÔ J˜Ø×,Ò,¨U°AÐ7sÀsÐ7sÐ7sÈsÐ7sÐ7sÐmpÐ7sÐ7sÐ7sÑtÔtÐtÑtØ š˜Ø×,Ò,­S°©Z¬Z¸Ð=zÈSÐ=zÐ=zÐUXÐ=zÐ=zÐtwÐ=zÐ=zÐ=zÑ{Ô{Ð{ùñ#|ð	|ð 	|r+   c                 ób  ‡— d}| j                              ¦   «         D ]‘\  }}|                     ¦   «         D ]w\  Š}t          ˆfd„|D ¦   «         ¦  «        rW‰                     d¦  «        sB|                      d|d|› d‰› d|› d�¦  «         |                      d	|d
|› d‰› d|› d�¦  «         ŒxŒ’dS )z`Zero '(' or ')' bilingual formats across action labels, button titles, and interactive controls.)Ú
vault_tab_Ú	shop_tab_rQ   r¥   r¤   Úskill_Úagent_c              3   óB   •K  — | ]}‰                      |¦  «        V — Œd S rª   r«   r­   s     €r)   r°   zmTestWebAppLocalizationEngine.test_zero_bilingual_or_parenthetical_formats_in_button_labels.<locals>.<genexpr>õ   s/   øè è € ÐBÐB¨Q�s—~’~ aÑ(Ô(ÐBÐBÐBÐBÐBÐBr+   r±   ú(z"Bilingual paren '(' forbidden in [rµ   z': 'r¶   ú)z"Bilingual paren ')' forbidden in [N)r   r™   rÅ   rÆ   rh   )r7   Úbanned_prefixesrM   rÍ   r    rc   s        @r)   Ú=test_zero_bilingual_or_parenthetical_formats_in_button_labelszZTestWebAppLocalizationEngine.test_zero_bilingual_or_parenthetical_formats_in_button_labelsð   s  ø€ àiˆØ!œ\×/Ò/Ñ1Ô1ð 	mð 	m‰MˆC�Ø$ŸNšNÑ,Ô,ð mð m‘��SÝÐBÐBÐBÐB°/ÐBÑBÔBÑBÔBð mÈ3Ï<Ê<ÐX_ÑK`ÔK`ð mØ×$Ò$ S¨#Ð/kÐTWÐ/kÐ/kÐ\_Ð/kÐ/kÐehÐ/kÐ/kÐ/kÑlÔlÐlØ×$Ò$ S¨#Ð/kÐTWÐ/kÐ/kÐ\_Ð/kÐ/kÐehÐ/kÐ/kÐ/kÑlÔlÐløðmð	mð 	mr+   c                 ód  — d„ | j         d                              ¦   «         D ¦   «         }|                      t          |¦  «        dd¦  «         | j                              ¦   «         D ]È\  }}|D ]À}||         }|                      d|d|› d|› d|› d	�¦  «         |                      t          |¦  «        d
d|› d|› dt          |¦  «        › d|› d	�	¦  «         |                     d¦  «        }|                      d|d|› d|› d|› d	�¦  «         |                      d|d|› d|› d|› d	�¦  «         ŒÁŒÉdS )z^All tooltip descriptions are single-sentence technical specs (no newlines, <= 100 characters).c                 ó<   — g | ]}|                      d ¦  «        ¯|‘ŒS )r±   )rÆ   )r®   r‘   s     r)   r½   zrTestWebAppLocalizationEngine.test_all_tooltip_descriptions_are_single_sentence_technical_specs.<locals>.<listcomp>û   s)   € ÐQÐQÐQ˜1¸Q¿ZºZÈÑ=PÔ=PÐQ�QÐQÐQÐQr+   r?   é   z4Must have at least 20 tooltip/skill description keysr   zDescription [rµ   z' contains illegal newline: 'r¶   éd   z' exceeds 100 chars (z): 'u   .ã€‚z. z' must be single sentence: 'u   ã€‚N)r   r–   ÚassertGreaterEqualr˜   r™   rh   rÉ   Úrstrip)r7   Ú	desc_keysrM   rÍ   rc   r    Ústrippeds          r)   ÚAtest_all_tooltip_descriptions_are_single_sentence_technical_specsz^TestWebAppLocalizationEngine.test_all_tooltip_descriptions_are_single_sentence_technical_specsù   s–  € àQÐQ ¤¨TÔ 2× 7Ò 7Ñ 9Ô 9ÐQÑQÔQˆ	Ø×Ò¥ I¡¤°Ð4jÑkÔkÐkà!œ\×/Ò/Ñ1Ô1ð 	sð 	s‰MˆC�Ø ð sð s�Ø˜s”m�Ø× Ò   sÐ,l¸CÐ,lÐ,lÀCÐ,lÐ,lÐfiÐ,lÐ,lÐ,lÑmÔmÐmØ×$Ò$¥S¨¡X¤X¨sÐ4zÀCÐ4zÐ4zÈCÐ4zÐ4zÕfiÐjmÑfnÔfnÐ4zÐ4zÐtwÐ4zÐ4zÐ4zÑ{Ô{Ð{ØŸ:š: fÑ-Ô-�Ø× Ò   xÐ1pÀÐ1pÐ1pÈÐ1pÐ1pÐjmÐ1pÐ1pÐ1pÑqÔqÐqØ× Ò  ¨Ð2qÀ#Ð2qÐ2qÈ#Ð2qÐ2qÐknÐ2qÐ2qÐ2qÑrÔrÐrÐrðsð	sð 	sr+   c           	      óv  — t                                d¬¦  «        }t          ¦   «         }t          j        d|¦  «        D ]*}|                     |                     d¦  «        ¦  «         Œ+g d¢}|D ]}|                      ||d|› d�¦  «         ŒdD ]¨}t          | j        |          	                    ¦   «         ¦  «        }t          | d	¦  «        r;|| j        v r2|                     | j        |          	                    ¦   «         ¦  «         ||z
  }|                      t          |¦  «        d
d|› d|› �¦  «         Œ©dS )zN100% coverage of keys referenced in client/webapp/index.html and HUD tooltips.r   r.   zKdata-(?:i18n|tooltip-title-key|tooltip-desc-key)=["\']([a-zA-Z0-9_-]+)["\']é   )Útooltip_divine_vault_titleÚtooltip_divine_vault_descÚtooltip_character_titleÚtooltip_character_descÚtooltip_bag_titleÚtooltip_bag_descz	HUD key 'z"' must be referenced in index.htmlr>   r!   r   z!HTML references keys missing in [z]: N)r2   rƒ   r•   rÃ   ÚfinditerÚaddÚgroupr4   r   r–   Úhasattrr!   Úupdater—   r˜   )	r7   Úhtml_contentÚreferenced_keysÚmÚrestored_hud_keysÚrhkrM   Úcatalog_keysrž   s	            r)   Ú6test_html_referenced_keys_one_hundred_percent_coveragezSTestWebAppLocalizationEngine.test_html_referenced_keys_one_hundred_percent_coverage  sl  € å!×+Ò+°WÐ+Ñ=Ô=ˆÝ™%œ%ˆÝ”ÐkÐmyÑzÔzð 	,ð 	,ˆAØ×Ò §¢¨¡
¤
Ñ+Ô+Ð+Ð+ð
ð 
ð 
Ðð
 %ð 	eð 	eˆCØ�MŠM˜#˜Ð0c¸CÐ0cÐ0cÐ0cÑdÔdÐdÐdàIð 	eð 	eˆCÝ˜tœ|¨CÔ0×5Ò5Ñ7Ô7Ñ8Ô8ˆLÝ�t˜^Ñ,Ô,ð C°¸Ô8IÐ1IÐ1IØ×#Ò# DÔ$5°cÔ$:×$?Ò$?Ñ$AÔ$AÑBÔBÐBØ%¨Ñ4ˆGØ×Ò�S ™\œ\¨1Ð.cÐRUÐ.cÐ.cÐZaÐ.cÐ.cÑdÔdÐdÐdð	eð 	er+   c                 ó²  — t          t          dd¬¦  «        5 }t          |                     ¦   «         ¦  «        }ddd¦  «         n# 1 swxY w Y   |                      |dd|› d�¦  «         t          t
          dd¬¦  «        5 }t          |                     ¦   «         ¦  «        }ddd¦  «         n# 1 swxY w Y   |                      |dd	|› d�¦  «         dS )
z=Verify i18n files respect FreeExile code hygiene constraints.r-   r   r.   Ni^  z-i18n.js must not exceed 350 lines (currently rÚ   i¼  z5i18n_catalog.js must not exceed 700 lines (currently )r1   r0   r˜   Ú	readlinesrÉ   r   )r7   r8   Ú
i18n_linesÚcatalog_liness       r)   Útest_code_hygiene_line_limitsz:TestWebAppLocalizationEngine.test_code_hygiene_line_limits  sJ  € å•'˜3¨Ð1Ñ1Ô1ð 	,°QÝ˜QŸ[š[™]œ]Ñ+Ô+ˆJð	,ð 	,ð 	,ñ 	,ô 	,ð 	,ð 	,ð 	,ð 	,ð 	,ð 	,øøøð 	,ð 	,ð 	,ð 	,à×Ò˜Z¨Ð.kÐ^hÐ.kÐ.kÐ.kÑlÔlÐlå•/ 3°Ð9Ñ9Ô9ð 	/¸QÝ §¢¡¤Ñ.Ô.ˆMð	/ð 	/ð 	/ñ 	/ô 	/ð 	/ð 	/ð 	/ð 	/ð 	/ð 	/øøøð 	/ð 	/ð 	/ð 	/à×Ò˜]¨CÐ1yÐivÐ1yÐ1yÐ1yÑzÔzÐzÐzÐzs#   ˜"AÁA
ÁA
Â"B1Â1B5Â8B5N)Ú__name__Ú
__module__Ú__qualname__Úclassmethodr*   r<   rN   rd   rk   ro   rr   rv   r|   r‡   r’   r¡   rÒ   rÜ   rå   rù   rþ   r¸   r+   r)   r   r      sL  € € € € € Øð"ð "ñ „[ð"ð2bð bð bðzð zð zðdð dð dð6dð dð dð[ð [ð [ðTð Tð Tð
ð 
ð 
ð{ð {ð {ð&oð oð oð(Oð Oð Oð"Að Að Að"|ð |ð |ð8mð mð mðsð sð sðeð eð eð,{ð {ð {ð {ð {r+   r   Ú__main__)Ú__doc__ÚbuiltinsÚ@py_builtinsÚ_pytest.assertion.rewriteÚ	assertionÚrewriteÚ
@pytest_arÚunittestÚosrÃ   r   r   Úpathlibr   Ú__file__r   ÚparentÚPROJECT_ROOTr€   r0   r   r   rg   rq   ru   r2   ÚTestCaser   rÿ   Úmainr¸   r+   r)   ú<module>r     sŠ  ððð ð € € € € € € € € € € € € €€€€Ø 	€	€	€	Ø 	€	€	€	Ø €€€Ø Ð Ð Ð Ø Ð Ð Ð Ð Ð àˆt�H‰~Œ~×%Ò%Ñ'Ô'Ô.Ô5Ô<€Ø˜HÑ$ xÑ/€
Ø
�tÑ
˜fÑ
$ yÑ
0€Ø˜tÑ# fÑ,Ð/@Ñ@€Ø! DÑ(¨6Ñ1Ð4JÑJÐ Ø $Ñ&¨Ñ1Ð4HÑHÐ Ø˜tÑ# dÑ*Ð->Ñ>€Ø˜4Ñ $Ñ&¨Ñ6€Ø˜,Ñ&€
ðL{ð L{ð L{ð L{ð L{ 8Ô#4ñ L{ô L{ð L{ð^ ˆzÒÐØ€H„M�O„O€O€O€Oð Ðr+   