From 0b1f0358b1d3034c5b790cdfec8523dcbacf461f Mon Sep 17 00:00:00 2001 From: Xenofon Konitsas <38171523+huskeee@users.noreply.github.com> Date: Fri, 21 May 2021 20:36:16 +0300 Subject: [PATCH] Create installer (#486) * Fixed a lot of installer stuff Fixed NSIS localization scripts, app version and added file association creation on installation * Fixed even more installer stuff Finalized installer and NSIS localization scripts, added pxo document icon * Update release.yml Added installer build and upload to release workflow --- .github/workflows/release.yml | 31 +- assets/pxo.ico | Bin 0 -> 106341 bytes installer/FileAssociation.nsh | 190 ++ installer/assets/.gdignore | 0 installer/installer.pot | 28 +- installer/pixelorama.nsi | 69 +- installer/utils/LICENSE | 674 ++++++ .../utils/__pycache__/polib.cpython-38.pyc | Bin 0 -> 47452 bytes installer/utils/installer.pot | 63 + installer/utils/nsi2pot.py | 143 ++ installer/utils/po2nsi.py | 182 ++ installer/utils/polib.py | 1880 +++++++++++++++++ 12 files changed, 3220 insertions(+), 40 deletions(-) create mode 100644 assets/pxo.ico create mode 100644 installer/FileAssociation.nsh create mode 100644 installer/assets/.gdignore create mode 100644 installer/utils/LICENSE create mode 100644 installer/utils/__pycache__/polib.cpython-38.pyc create mode 100644 installer/utils/installer.pot create mode 100644 installer/utils/nsi2pot.py create mode 100644 installer/utils/po2nsi.py create mode 100644 installer/utils/polib.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b5b04964b..084a6776d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,7 +8,7 @@ env: GODOT_VERSION: 3.3 GODOT_VERSION_MAC: 3.3 EXPORT_NAME: Pixelorama - TAG: v0.8.3 + TAG: v0.9 BUTLER_API_KEY: ${{ secrets.BUTLER_API_KEY }} jobs: @@ -18,9 +18,9 @@ jobs: container: image: docker://barichello/godot-ci:3.3 steps: - - name: Setup WINE and rcedit 🍷 + - name: Setup WINE, rcedit and NSIS 🍷 run: | - dpkg --add-architecture i386 && apt-get update && apt-get install -y wine-stable && apt-get install -y wine32 + dpkg --add-architecture i386 && apt-get update && apt-get install -y wine-stable wine32 nsis chown root:root -R ~ wget https://github.com/electron/rcedit/releases/download/v1.1.1/rcedit-x64.exe mkdir -v -p ~/.local/share/rcedit @@ -42,15 +42,19 @@ jobs: godot -v --export "Windows Desktop 32-bit" ./build/windows-32bit/$EXPORT_NAME.exe - name: Copy pixelorama_data folder 📁 run: | - cp -R ./pixelorama_data ./build/windows-64bit - rm ./build/windows-64bit/pixelorama_data/.gdignore - cp -R ./pixelorama_data ./build/windows-32bit - rm ./build/windows-32bit/pixelorama_data/.gdignore + cp -R ./pixelorama_data ./build + rm ./build/pixelorama_data/.gdignore - name: Zip 🗜️ + working-directory: ./build run: | - cd build - zip -r windows-64bit.zip windows-64bit - zip -r windows-32bit.zip windows-32bit + zip windows-64bit.zip -rj windows-64bit && zip -r windows-64bit.zip pixelorama_data + zip windows-32bit.zip -rj windows-32bit && zip -r windows-32bit.zip pixelorama_data + - name: Build installer 🔧 + run: | + python3 ./installer/utils/po2nsi.py -i ./installer/pixelorama.nsi -o ./installer/pixelorama_loc.nsi -p ./installer/po -l "English" -v + makensis ./installer/pixelorama_loc.nsi + mkdir ./build/installer + mv ./installer/${EXPORT_NAME}_${TAG}_setup.exe ./build/installer - name: Upload 64-bit Release Asset 🚀 uses: svenstaro/upload-release-action@v2 with: @@ -67,6 +71,13 @@ jobs: asset_name: ${{env.EXPORT_NAME}}[Windows-32bit].zip tag: ${{env.TAG}} overwrite: true + - name: Upload installer Release Asset 🚀 + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: ./build/installer/${{env.EXPORT_NAME}}_${{env.TAG}}_setup.exe + tag: ${{env.TAG}} + overwrite: true - name: Upload Release Assets to itch.io 🎮 run: | butler push ./build/windows-64bit ${{ secrets.ITCHIO_USERNAME }}/${{ secrets.ITCHIO_GAME }}:windows-64 --userversion ${{env.TAG}} diff --git a/assets/pxo.ico b/assets/pxo.ico new file mode 100644 index 0000000000000000000000000000000000000000..eafcb2eb4d7ef4f5fce0ac365c1cb9d3e279a1ae GIT binary patch literal 106341 zcmeHQ2|QKL7k`!tZKQpVLQ;tAYf+MY%T5%DNJxs%hAfGaw9rDOefcFNBx$iEDp|7c zDoM7&{AV6dy0W~i{kuM&^O-mI&6{)1e9t*EbLQTQK%gLuBFvvpfaQF`C3*rugFql~ zakbYM5eTjDnuJ7Kooyw75XMI!xVW^}X($LOJK+!DANsx3Tmqrqg@V8a>+>N=Ktk7v zf3TnK{tg-LCP11EsR$BiBS%;c1d;k9_KkRS5d&R=MC9*bunX+hu>X-Y$c>s{iKz|zEGcitE5&A`j{_;n9Bi@!`lHiTIDoFQN0cKtz9sG6O*MTiIX~1mrTcEZ=zfFct(&0En1Lb89i*|eH zFMmT`amZ5z6dqj<@=tC8uY8PvA+ID*WP!HGA`J9|ApdyCKk*ybVpRfkwO;|1WeNkz zKlPw8k$2Xc4DjOl8=x;F4-^(boMC)bZLvmPNqOxB%$tqzn%KQ>GyELpgYv zUIr`_je)M9JWyd5g=4J%bxr|#8w!9vq`}~iFfV}o-{+Kpw?z#g=}j4kicSN1PzGfV zc_L55Ti>V<=;;jX__x`gB1rQ!0vTyk-_1YDkf+8{ zN#Z&O#hPE~PuE|4U;k(O1p(BYA3hU?FAxc+?!Wtah}hyBN&k3SlB zgaw-Sklm~Qw2M+VJ^K}&hK|!9Z>quPFGA$qrngdZ$#3+ zZ~Kp*{@p(QP-ise?f%fK-d*|cE+TLL)#l&G_K5ZeZH*kgf5)Hf{b##>cluqQ_5K}y zhy&UyqWxMQiPWA{qJ5qt!jjZ|w`D$oFlGp89^{3{xs50Q%2U8p2f+|hN1kFKtn~8N zZm&~eS@CncNcs<3iDW@C^&&k1`5L1CEBO!T?tnfx!~}W5aU{_nk3W)Ki+eROcA@w+ zRD^gU-t9X4x%_b4k<6sE+EA5s&8O`;48Qy+Z-mDE4V1mWUmd*lR|O^k zLO|!ZKquk{I)1Bx-ljB|ODqJsFrSC8Amy=thr?ixe`lL96_fyP4?+3;p!|NSV3T*( z@z*)D2AEpC0h=v~fQji_pt^j0n>}@7OM@*x$!5^FBiZwgs1xOX13&8t!83$k>|Yq@ zNUQ^Ki6tNDiewqo84#<{9vi!(z z+w7+iWI!F>Lmb|hHoF}WfB!4J_=Xhlx z?$vu3_n~icL;qa??MIJfKkz(HFTKaL#9+tIU*=ySc>WG`KsdZTVF&M!LKe^2t0ViX+z7TB%|`feqpcaSDSBK!QYCtl>l(7woziBVN<`a2gxnWwbN zPC4S`?_^_8);`Ca&Y&P2A$6Sp4#oKQ*Jg%O{(-QW;gY}YUZ6jVGo5Ab{JD$$4AA*? z=Xl-`|6z>$(>T*v=FXq7^20ju931&Uzv$Qbv~!<-6lXfi-1+nG*$+I zb-Ogj^&iw2*@4qwjdK@3$-FRFat&3#WUAYxIaW?`_7=m@7AtQTg~+^s?Qb}AB8Mkd zUNY6~(j40#IkF5#TdcfY6e9Biw!h)fi5#9-dC63_OLJ_0moyg&dm6uF)yEMo4M~*DR(H1Lj7lp{Y zfbDNMbRvf*R$emI?b00EA33rNM_a7CT@)ho0=B>5(1{$LSb51*w@Y(uf8@wA9Br}k zc2S7T3)udKLnm^0V&x@M-7d|s{gETfaJ0qB+eINVFJSu{4xPy1iIta3b-Ogj_D7B^ zs4eLpTvDkM0Po-KfqvdY*>*oAx%{y5c2S7T3)ue1m4~D^sq9I2^c}8$=lf{$f4$!s z+EZ`ikHw2jxVtpR_SYMK?Dr(xN#*}L{v#j6XTWM07b zhm{vw?@n*hb!>ZVy|eDsY+Vgu6>~Y=2mO z*gB4X+cS5halqO?wvHV?EZ*2Uwmr6v#j6XTWM07bhvkQ@lk_H)Sbec|?C-JqV(ZxU z*g95TGS%(U9NQn3AGS`?n^a=;#n!RE$Lfo%W7}ivSb4iBMCJu-e^`FlI!SL*iPaZd z$NnCxFSd?tkF8_nB~#rl&9VJq`Sn&OjRUFtf8YOL$AhFdsl@fi^FKY=0J;8-wKuH4 zc5!GjFJSw_@*8P&th{8Z+od_SzmcXdRvxUpT@)ho0=7RazmZnQ%1frYU7BP28)^Dt z<-y9^MIkaTVEe=J8|XTgUw`Xls@tVGmj6KOi|r4~ufO#!3XypM%O5N6K-aPS`dcSc z-7d|s{0CZJY=2mO{jGOVh|CLE{#bbjy53)Y*x!?>ZkOiR{svlKY=8abhy8sQg~+^s z?GG#OK-c@r5BqyE)$P(8+uuOzi|wz!{II|8q7a!Eu>E1>9q2l_{IKoGRIk_O*!~Av zUu=Km^24_8RUtAPVEf0)JJ5A<`C;3Wsa~(mvHcIUzS#cA<%ezGt3qTp!1j-occAON z@xy*kraELd$MPF!eX;%Z#t-{F*$R>Q11vwRyaQeDjUV=VGSwlwIhNl*>x=ELH-6af z$ySKWA7J?ncb!b}``vuF^~Lhy-NeffB43}CtQZ=I~;!;e?0#GyEZW5{U83%Q-IT- zj{#g`^)#3KCHOnxHbAx?w2j%{j6eKe%fMN~3OK5mL$bh$tT8YrjvX4q9&BJl`adb& zBkav2)&o8CzeFJE^R2_F4`V#NDwHn2oe7;4tfVO zPCbtS8Y?trXzWnz^xV+xTrh(1cO=Gpqyy3i$G>OHJNpD_yxQ=$_)Go24Sy8#NxEUr zMDhCX-+MtCFEn;&-Sl_He;ofmwQn5%-}}X%j_2R@I~;!;e`EuA{Qo!Y0QdiY6L-=) zh2xLokH`Oi*9LI^|97!Ra{!J%jz1p%|6Lov{r}&^9*_V3HugCFIR1G2|F>-b#~;TZ zU;qERHh}y8zl%M-{`@GzR1L@JuHj05|ETm8eSzx_e3}`qCfc1`oKy!B= z$&nvuI`RRn?V3PaF9_(UJOnzb4D+AT@Lh(N+A^!$-uZ27;G#j+JGwGruMl3$DcG040ilS>pe8*qnMBU9${a6Oal}i zRVPw0#JWxk3;+eA^w37`#uxI|3n-9dRCDTe~A5t5{P|a8yS}XWCJ=d4xo9V zZGC{_Kcx7>wHd-6#eC%V2z%21;w^=tZ4}kaw^!)!RfBD~y$N7NH zafrXi2B2pd0rU-_-y1^gH?;rHdZHZ^6XyaP|E}-<9TNY?@keX!5&!;|{;dDI+uzH< z*e|QA{CWJ>Ib;ZQw?+V6{bERkK-VC@jbJR$gE2u%DI9;rb8ZuVWDb zbo7cK6#^aoyf!j`pACwDrg8*O;oSgp47_bN&?QgsXZeAD9sl0;_sH)N_TK7iK!URp zod4mv&mS}Y!}VW`w*qi*l>q9y1%O6h_kP57d~mP00&H+H1~$%7z|pl5QaRY|T1q6x zy=B00Zw1(Hl>qc)_7QCW#erXQ1b?0%_}B68ZGVr}e8}$+_V2yaKmo+Q0IvBu;yiUI2ZM?8FAf2{~qXKCrOwd0+)* zK_I_C09dP;fynzW!H18{AgA&>$SP|jQcgu9_)ytMv;hSe^1)y zqnO{>@4Lkxt``*08344azL3H~h(8m=U)>Dchxq4IHiPVn?;x|Zfk@fqjR4sIiUVja z(1Gg#U9>+Op!@)iKU(*7zrQEp++9iZe;EIX{tt5ms4tW=>T7;h0ls@veqkH__aa_` ztnwcqv+O&_C~hDUvI7(g(0qW_1ZZ7|_67r$AHea)@kciBufG3_nBVP_|Eq!gBdT4f z&`%W<`>R6#mj^bIl7H_18hwcWxc?9M`7e3+tAKo8OiKruuhT$gYATVk)6+oCn{;sh(p6xsYzWtKZSSA&tN$y)^L#}X z0iZ3q30Rvy0*?D&{%3Ee{15bb_b>x5B z^T1vFet^ygas0`^ANqY8_RS!pq=`tG<;@WPR)~L6zwt+TJw3xB;{Lwl{HOgK=$DvJ z!XEekj`!97>HU9wivL9acO=GpqTeIzN&9oSS4Vqxw0B229JH7JBmal@cA%VYTVAiz z`G04ufA<-W|3n+W@rUQbe{g>g9{>9q|9kQOdoO^Mswr5vUcY6bCv8{$Cm; zbdNvwzD?p8KAit?|Nrg(gNEz;9{>M$fA9YpkoaE#<3GgTQpE(w&f_840GbCnh`DV} zK)#0K|67>5y*T3WFKr^`>!ah|0*{@ZL>oYH0Idax1os4JUtlJy4>Wn!5Z|}^OB}%c zzgz6l7b6}2jFLw19@4AKj{w;KiUUM~>p`?8ydRkaA|E7!Ge<8GZGbo*pt*oF58(L! z-T1ff1CR}%bGs}!7ohzBiE^r1!G}+PXamIjK>J*P)(C&Yw#!80j4Czgr4#PIR3wbz2h2=f2Xq^JpVH|=YKss z--XWi(HU>vMJJGz);0f2$^)bFeF=>F7|Je(@gL@g??=G+uLjQt;Jsexp0A(xhQYPq zB9#B{E&o^W^PVr#y{=WCT1PLiodbJ<&a%Ki?Ao|5r846{zy|1^=S<{4jq1C!80E?*Zs{ z{->AcyC2!hgGW2$K)jD7NPBpTXagt?kj{Uz;5jg&*QqJs@#{oz|Md$H*@+&eCxVA> zlR(Igi@2z5^NKf1m4<8L>oYH0L=yH-Va21=pGPwHvIIWJveg7 z9N1qrA(8_u+sG8`3WxciGdjTVpcK&DB>${`_ahJAnE-|C8|VuI%l62!H;+W2-(W9|n8>-yQCx z>p1=$@7wyne*T|?H?}0=-$Q-?fB(y%xP3>bBf|fC$PeJ}f9im0P(BPh{=MV}@b|wB z3hW)74mbY2v;Sh1e+kmeB}2CdvD`Cz{{vD29xIE!7saw`8fWAfO+RN0?GeTtVj4G?1vtI z$L{8Q9RDH2znArQXDo)}Gamm3!4^9EKaPLb{New5{*N5|2YVgxn>}CWID`BDF#CTS z{)4eL#MghF*JMNaxv&0@_GV~*?xU{O|2LGFcjna_{-l_Xutzx>Qod%Oazmv4 z@chrv=KDxC&^P>@h<=Z-Lb+O$vnBnmza^gk9Uk1t!{3n@^AY|CuYtxN^@sZIW&eS% z|At}xM~eRl8x-?N{!X%={+6WvP~SNILmvxz8-E=C!F~T*fBmPwzaL8cNxV9jr1=Do z|3mNl$aY9rbQgaI(*N6d{knEU_~YmQ!-M@$;@|E41uNDO*M6pAYG2^_Q!2E#SV(BC zN1}N6IRTPCBu+@gdvd#GY6s^Ei61P*AR(DL=OjDsR~W4IQGermZ@(XG|0ng|+x-Oy z`@NdmE#Un>f)Ib<_VGqzjz}C5!hRwo9RC5_2axa?toTog`F*&*;4#Eq3=#p+Nckh# z8!U1E|AYNsXZz~q{sJ8T0rczM;y&s7&iMRp{|$YSILH0z{MK9l(631I>9FK?dW$o- z|Ks?#+W;Q_dx`%fU+-Lw^!+{V|84jB55)Sf4fnrme~;roF!=wi`+FS!{^3uG^?&RB z9>;$m@E0OJH-z?gJv=`!BKLPV{sV!3+yC$W8~67({sV{q@A3yDeSeSRkK^By4Um)n z8I1d*Nb9S?Dtp_Xch3L)PQTuM|8MyJ;N2fNSh$n=?d|-%4gc=->+Sdd2L6u3d*cV= z{%Dk!8B9Vxji|f(uSx#j?fu^){QSPB*xr}#dW-)g`ySlqBL@3f{NA6x=uN+odVb#* zjC*R+SNxG3kYXjtXa1hDPtRZVrr%$=zwZfq^d0&5x1E>$J=p)0mlVsoD|^xDpU1zu zar;NEkI3`;-u%0_?;&=i81UQk-+u&K(mfhIl{o%C?`7#}oc>?G!|})QM=^3p@6YWm ze)so#l0LA+vZzw05=zXMor7@%T9cxIN(h@Y5Oy1l%5Qdl*7{fadngcn@(M zvFpr#i0edlo%jz*-9t%X&3L+*bP(0?Yh|QV2m}iFk%B-o8Wy_SFPRVsbNSXvNviG4 z`gD5Tjv`sVFmanP&wW=O`r#$waOdGyTZJu*qMx1KuR7JTUbHS7yrwe}o=oQ^Hauj^pTEeRgUM}_7WJg6S|3vp$+FtF zDQ;K4M@`I$jw#~VOtHCKU#7ybm@RK=1wJ_Qh&N z8C9(@)P%Cxpm8FIy5{vQx=GW|WAqY2xUAQ$N0%$p_|5h`-=o!HAx&MTyIW`0(_o=o zo@nt;t@3OVDN@DfZ9c~YNV_()>Z|K{-6|K{Z~K{9iE3i3Q}hj=mKpQ*NzzK#99c0< zQC&SL{7`r}4fkDU56uSWDM8EFnRb>fPV_U=QVCd;`y;ga;^7uYXF4YL=fc%F)i2LA z|ERE~`?5y;w7<~KJi8B?bSo>H`D623z!=9kYD3NV=_k~u*xTFZh~}uioig*k)4@Tb zKb}aaxj0{e&C@U4o^{)!~*Wu^52nU1TyxW%8HAbgxP>g~5Cp|U`Q zFv8JIYfs6!JhiW#{;IY0?k$eVWwZUaf1S`Ma9ZJ&1n}VvUlSnT5*opxSM9U09xM`X zrEQFP*1GfX(-7(q^O@o73r2@OFyafY;X28%Qo_$+=f|);{+HOpvwZ5)D>lBcb&C?Z zxnaIjMMK2b)=hK5Lc`B&6|}UUIP+^u43+7)tMfkkZ#ccPY>C>}Q4HyuC#{?xA2?er z6`+@+80_>|uSa z!Jb0nF$I}(mZVE>Y z3>Gfnh&x~}Qn|T`7P`Z}vnNe&2CXApE((iznrmrRNBs`^$7pHB(K*R1)Pc#R?*5^7 zOZHqnBQcGUVC>6VM}7a{`Ed5As>us8xteuEU0WHKdr79sGg@-E&{JB983<7Dn2Wxy zr2S?D8BS7{UmU;Sn=(bLc-3QyC}_Jr{^D6 z(q4tVdC@_J(6PB?ubzoLCwY*Y4wgc&A=|B8l(bNhyPBdD$mQ)&L98W?;_|xIs6iYQ zOpFkq%W4Vr4@o~*7|k7|zSsNHT(*03hO>QExH+`G4&(Kp63G(9 zcjHz}@`3+&zg|=_ESp`pajBc(=5vOWhO@^>XUEQ9cr2`_bySX3b^d{ju$ah*i2K(g z6$qCtV{Mm*a=Z-Xh%24#wx8C;T4`o&rr;W_fK$=J6ox{lLzJgU3+A08Y~&0PILo1u zop}4>w=BmT$gomHQ9kA7ljF!Nx|Z=$k@r}uv~ z#h+Gxol*6k-Q||3`Q_@SHM%JuZ5E%-aCP+jVQ!^4`mO-MIEweYv;WkS=4H;FEit=O z_Ktf{#19!~r(fQYKJ~df46+}Lu3Qc>9!CWMHm5d|H&9;`FQL9Zsg%b>K}D{)bg#)7 z+0iq%2;8A{xy6`#Aot_--3{9&##->LImJ}+l!JI3<%%(6#dHBc%NNs*Rti{T zz1&PZc!75-1XF}jebN3@A&?+Zly9*bhT+?4FK7Z<7Osk=j@`+Zw^FS6T^ZeGudNwA z+~<@t*k@!$6B5elk8FCD$2F($aPaxG1cAG%XRY!@AUo6aI@+igveCPxq!;+{Ej=>3 zCD1t_c<0A}&j(8=rzcG1rHtSnV-x}h1=g^tD9g*JbuFIe`4{-{>SG0viGfrQy! z*CPz*4GI-f7DxY(X{xFIVw$%~IV|Rx{Nj6aCEe8omML3PI}jpQJCth@EUWokD3y=0 z<)2qdDtNNAxpj4;RYaVkde8)3%9|_(X*4cehhJtmyu20@&SX&PVB{|rWWhYcEtWdp zmp6A0#i>;<*9MI~<#ZT)VOEjzJ4V0i6|V%n$b12I`o}M(Fsb@dK0WJgtUT&N1cBy~ zOyzrq=5b+6r`8l)WOKCTN~Tmwd@xT!hBBYpBvYzUm%TbFe^$nrcQq3c{`6~Y_83Sg zGd*3{M4`5T`g<6`n{WZh(=Sfr&N}cd!<#m>G?js7FX#TaRTYMnhKCl8m3)`Dmv*mx z^1{_81kNVyy}9{X7(e$Ef$7lZLK&%it-|RdzrCrnNq-PyzMeuOZkxdS=~+8}s4E}4 zw}Sg*%@MJ&64pV~Ok0E(+_sBlHm2Oh5u`AWIo>y^m0rU54CU3B3tAK|F0xXK!v2de zX}R{v_GPYpsju!ts@tPNHcvjc(%nppGrpMZstx12nZeTY**eHNTLuK9-G1xw8f8PZad0;W zuhw%}Y0HNu8{z+)3kBFY_L`_Ezdo?usfurHSzf5ycY~}u4=&xD7V{y_*;hm8UfqVE z#4ja*1d)@b#kFoiqIrvMIC~wLevIDEL7lTcChAi#J9Ws^iMt8$K<3LmP5<(DoJ6UYWgm`8ZB~_Cq za8pyWyjRQ;*_XA|f`Z~jA6xdPH^db`Vv@XjLiu@Z1NS_KlV(!U4V2YU+w|$g z75q4N%}sw^eIR$S$6Lnt;w*%RU;G^Rs2hELAN}RjsoOG}phc7mNxl{H{q|iym z{}BD|HTO&`Z%8ruXe(SQ9>dP0EEZxr%CP0_GQ|L6_nf>}k@xQxRJE*_HZ3A2Z+$$y zPH^lN&PweclrocPCQ!=wT2k@vvDITfZyV>P(%ACw&AZ?wXVaw9l;&wD8iGK=$GEXy zo6?_DFfFBfmidEL+*zdLaLlP%x5H;9Rx@u>zNi~=>FP?F?ARyHxwYG7q_SV&SXKQ!0c@i9;`i;kZ3X-GCGsDC zGzvXCA2yREKzS6cg}%u7mR9Z_O9jdo4v=s z;bnzG@sl6V4_7nK%Uhxvtm;%4Ciccj&1HGsnH>jcS1Rv1MKe+QfE-hB@qYjC3-_lK z%f5DV7Z@Y0o7H$n(9TSEzJh8iCmo| zttQ{pBptUmI?QA93|f0cD28+Ljr@o+@z*s#W)79~LV4v=U#=g#%(=LxvD`Okx}>-Uo4#_2E)H6zb}b$kuemboRfc(v)EA3vd~Ihp#tzu3i0i>=$someMr zJ9(e!#h7=Y{F%+V$Mb}@F!D=e)$huvYY}}n$87e##o=FW)x&jg28_ay9|K?I$x*0W zr1EAMf5exWVwD_o3}xLr^C~^lg7hQK(sImZbEqV#Tm!QTpYsWtxqlkVym{7j&$4UR z_$>2gJ!E3dj>>@w!u1uWNw#BaO8Rl5Ry)DNdrmsKJj=SpV7By!Wv+|m4~4BCTrVDP zuK%zdu6u7B*hDq+ms#GCKy^`mbylInr*C=>GHpu*X7R8Ps5XfPg9>%R`U?UsOj3>o zH7TK&#KxYPx0HpjFLdpisX6yfzo&J75_xp`8*R#j&D<2z*@E`@taQ1y(shw{a|-Rm ziBs-4NULY;_f2>4RyLyKnjh4(r?T-#QHVK=IodjR_CTF(`B5Nmn&z&WpRdo$`Y;V z^Y=e*S+$JjYe`V%)s<6DRRsw0B)HJV8fpqOOPqLRzHFIgBkZ}>xwY;F63s%tKS!r@pIooPyPZkO?_uJeun z(>mc#yfJ0q>eT0YAA&s3l|^>Is96Hpz)!~gU(oYa$R;c}$%z!)CV6Qz%X(jw5z*=KU3c z^CvUiXG@|?qI$#q_PMyGkh7_Maf0KKIOa{RM<>*nRWfDYdsaz58|p}v?qwrcaQ0x; zi7$1_M1`y`@2;bBp^-jvuEB(-pz6^!(S2@09um7!%yp>8X6v6?#lo0VA>3@Q+Pv1a z!NK_JRzCjm4A;!>hQEHGFzf22phepZ

j`?o88PHASTi)Un7DqH~r=pZW1DI+uBG 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 W|IzHwgP+k218b!fq^_?t-1|S5{FWa8 literal 0 HcmV?d00001 diff --git a/installer/FileAssociation.nsh b/installer/FileAssociation.nsh new file mode 100644 index 000000000..b8c1e5ee7 --- /dev/null +++ b/installer/FileAssociation.nsh @@ -0,0 +1,190 @@ +/* +_____________________________________________________________________________ + + File Association +_____________________________________________________________________________ + + Based on code taken from http://nsis.sourceforge.net/File_Association + + Usage in script: + 1. !include "FileAssociation.nsh" + 2. [Section|Function] + ${FileAssociationFunction} "Param1" "Param2" "..." $var + [SectionEnd|FunctionEnd] + + FileAssociationFunction=[RegisterExtension|UnRegisterExtension] + +_____________________________________________________________________________ + + ${RegisterExtension} "[executable]" "[extension]" "[description]" + +"[executable]" ; executable which opens the file format + ; +"[extension]" ; extension, which represents the file format to open + ; +"[description]" ; description for the extension. This will be display in Windows Explorer. + ; + + + ${UnRegisterExtension} "[extension]" "[description]" + +"[extension]" ; extension, which represents the file format to open + ; +"[description]" ; description for the extension. This will be display in Windows Explorer. + ; + +_____________________________________________________________________________ + + Macros +_____________________________________________________________________________ + + Change log window verbosity (default: 3=no script) + + Example: + !include "FileAssociation.nsh" + !insertmacro RegisterExtension + ${FileAssociation_VERBOSE} 4 # all verbosity + !insertmacro UnRegisterExtension + ${FileAssociation_VERBOSE} 3 # no script +*/ + + +!ifndef FileAssociation_INCLUDED +!define FileAssociation_INCLUDED + +!include Util.nsh + +!verbose push +!verbose 3 +!ifndef _FileAssociation_VERBOSE + !define _FileAssociation_VERBOSE 3 +!endif +!verbose ${_FileAssociation_VERBOSE} +!define FileAssociation_VERBOSE `!insertmacro FileAssociation_VERBOSE` +!verbose pop + +!macro FileAssociation_VERBOSE _VERBOSE + !verbose push + !verbose 3 + !undef _FileAssociation_VERBOSE + !define _FileAssociation_VERBOSE ${_VERBOSE} + !verbose pop +!macroend + + + +!macro RegisterExtensionCall _EXECUTABLE _EXTENSION _DESCRIPTION + !verbose push + !verbose ${_FileAssociation_VERBOSE} + Push `${_DESCRIPTION}` + Push `${_EXTENSION}` + Push `${_EXECUTABLE}` + ${CallArtificialFunction} RegisterExtension_ + !verbose pop +!macroend + +!macro UnRegisterExtensionCall _EXTENSION _DESCRIPTION + !verbose push + !verbose ${_FileAssociation_VERBOSE} + Push `${_EXTENSION}` + Push `${_DESCRIPTION}` + ${CallArtificialFunction} UnRegisterExtension_ + !verbose pop +!macroend + + + +!define RegisterExtension `!insertmacro RegisterExtensionCall` +!define un.RegisterExtension `!insertmacro RegisterExtensionCall` + +!macro RegisterExtension +!macroend + +!macro un.RegisterExtension +!macroend + +!macro RegisterExtension_ + !verbose push + !verbose ${_FileAssociation_VERBOSE} + + Exch $R2 ;exe + Exch + Exch $R1 ;ext + Exch + Exch 2 + Exch $R0 ;desc + Exch 2 + Push $0 + Push $1 + + ReadRegStr $1 HKCR $R1 "" ; read current file association + StrCmp "$1" "" NoBackup ; is it empty + StrCmp "$1" "$R0" NoBackup ; is it our own + WriteRegStr HKCR $R1 "backup_val" "$1" ; backup current value +NoBackup: + WriteRegStr HKCR $R1 "" "$R0" ; set our file association + + ReadRegStr $0 HKCR $R0 "" + StrCmp $0 "" 0 Skip + WriteRegStr HKCR "$R0" "" "$R0" + WriteRegStr HKCR "$R0\shell" "" "open" + WriteRegStr HKCR "$R0\DefaultIcon" "" "$R2,0" +Skip: + WriteRegStr HKCR "$R0\shell\open\command" "" '"$R2" "%1"' + WriteRegStr HKCR "$R0\shell\edit" "" "Edit $R0" + WriteRegStr HKCR "$R0\shell\edit\command" "" '"$R2" "%1"' + + Pop $1 + Pop $0 + Pop $R2 + Pop $R1 + Pop $R0 + + !verbose pop +!macroend + + + +!define UnRegisterExtension `!insertmacro UnRegisterExtensionCall` +!define un.UnRegisterExtension `!insertmacro UnRegisterExtensionCall` + +!macro UnRegisterExtension +!macroend + +!macro un.UnRegisterExtension +!macroend + +!macro UnRegisterExtension_ + !verbose push + !verbose ${_FileAssociation_VERBOSE} + + Exch $R1 ;desc + Exch + Exch $R0 ;ext + Exch + Push $0 + Push $1 + + ReadRegStr $1 HKCR $R0 "" + StrCmp $1 $R1 0 NoOwn ; only do this if we own it + ReadRegStr $1 HKCR $R0 "backup_val" + StrCmp $1 "" 0 Restore ; if backup="" then delete the whole key + DeleteRegKey HKCR $R0 + Goto NoOwn + +Restore: + WriteRegStr HKCR $R0 "" $1 + DeleteRegValue HKCR $R0 "backup_val" + DeleteRegKey HKCR $R1 ;Delete key with association name settings + +NoOwn: + + Pop $1 + Pop $0 + Pop $R1 + Pop $R0 + + !verbose pop +!macroend + +!endif # !FileAssociation_INCLUDED \ No newline at end of file diff --git a/installer/assets/.gdignore b/installer/assets/.gdignore new file mode 100644 index 000000000..e69de29bb diff --git a/installer/installer.pot b/installer/installer.pot index fbfe2417e..930566749 100644 --- a/installer/installer.pot +++ b/installer/installer.pot @@ -1,63 +1,63 @@ -# +# msgid "" msgstr "" -"Project-Id-Version: Pixelorama 0.8\n" +"Project-Id-Version: Pixelorama 0.9\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-08-08 22:43\n" +"POT-Creation-Date: 2021-05-07 04:44\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" +"Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Language: \n" #. SecInstall -#: .\pixelorama.nsi:72 +#: ..\pixelorama.nsi:84 msgid "Install ${APPNAME}" msgstr "" #. SecStartmenu -#: .\pixelorama.nsi:73 +#: ..\pixelorama.nsi:85 msgid "Create Start Menu shortcuts (optional)" msgstr "" #. SecDesktop -#: .\pixelorama.nsi:74 +#: ..\pixelorama.nsi:86 msgid "Create shortcut on Desktop (optional)" msgstr "" #. un.SecUninstall -#: .\pixelorama.nsi:75 +#: ..\pixelorama.nsi:87 msgid "Uninstall ${APPNAME} ${APPVERSION}" msgstr "" #. un.SecConfig -#: .\pixelorama.nsi:76 +#: ..\pixelorama.nsi:88 msgid "Remove configuration files (optional)" msgstr "" #. DESC_SecInstall -#: .\pixelorama.nsi:178 +#: ..\pixelorama.nsi:200 msgid "Installs ${APPNAME} ${APPVERSION}." msgstr "" #. DESC_SecStartmenu -#: .\pixelorama.nsi:179 +#: ..\pixelorama.nsi:201 msgid "Creates Start Menu shortcuts for ${APPNAME}." msgstr "" #. DESC_SecDesktop -#: .\pixelorama.nsi:180 +#: ..\pixelorama.nsi:202 msgid "Creates a Desktop shortcut for ${APPNAME}." msgstr "" #. DESC_un.SecUninstall -#: .\pixelorama.nsi:181 +#: ..\pixelorama.nsi:203 msgid "Uninstalls ${APPNAME} ${APPVERSION} and removes all shortcuts." msgstr "" #. DESC_un.SecConfig -#: .\pixelorama.nsi:182 +#: ..\pixelorama.nsi:204 msgid "Removes configuration files for ${APPNAME}." msgstr "" diff --git a/installer/pixelorama.nsi b/installer/pixelorama.nsi index d827acdfe..af33aa2ae 100644 --- a/installer/pixelorama.nsi +++ b/installer/pixelorama.nsi @@ -1,17 +1,19 @@ ; Pixelorama Installer NSIS Script -; Copyright Xenofon Konitsas (huskee) 2020 +; Copyright Xenofon Konitsas (huskee) 2021 +; Licensed under the MIT License ; Helper variables so that we don't change 20 instances of the version for every update !define APPNAME "Pixelorama" - !define APPVERSION "0.8-dev" + !define APPVERSION "v0.9" !define COMPANYNAME "Orama Interactive" ; Include the Modern UI library !include "MUI2.nsh" + !include "x64.nsh" ; Basic Installer Info @@ -50,11 +52,15 @@ !define MUI_COMPONENTSPAGE_SMALLDESC !define MUI_FINISHPAGE_NOAUTOCLOSE !define MUI_UNFINISHPAGE_NOAUTOCLOSE - !define MUI_FINISHPAGE_RUN "pixelorama.exe" + !define MUI_FINISHPAGE_RUN "$INSTDIR\pixelorama.exe" ; Language selection settings !define MUI_LANGDLL_ALLLANGUAGES + ## Remember the installer language + !define MUI_LANGDLL_REGISTRY_ROOT HKCU + !define MUI_LANGDLL_REGISTRY_KEY "Software\${COMPANYNAME}\${APPNAME}" + !define MUI_LANGDLL_REGISTRY_VALUENAME "Installer Language" ; Installer pages @@ -98,24 +104,31 @@ SetOutPath "$INSTDIR" ; Copy all files to install directory - File "pixelorama.exe" - File "pixelorama.pck" - File "libgifexporter.windows.64.dll" + ${If} ${RunningX64} + File "..\build\windows-64bit\pixelorama.exe" + File "..\build\windows-64bit\pixelorama.pck" + ${Else} + File "..\build\windows-32bit\pixelorama.exe" + File "..\build\windows-32bit\pixelorama.pck" + ${EndIf} + File "..\assets\pxo.ico" - SetOutPath "$INSTDIR\pixelorama" - File /nonfatal /a /r "pixelorama\*" + SetOutPath "$INSTDIR\pixelorama_data" + File /nonfatal /r "..\build\pixelorama_data\*" ; Store installation folder in the registry WriteRegStr HKCU "Software\${COMPANYNAME}\${APPNAME}" "InstallDir" $INSTDIR ; Create uninstaller - WriteUninstaller "$INSTDIR\Uninstall.exe" + WriteUninstaller "$INSTDIR\uninstall.exe" ; Create Add/Remove Programs entry WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \ "DisplayName" "${APPNAME}" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \ - "UninstallString" "$INSTDIR\Uninstall.exe" + "UninstallString" "$INSTDIR\uninstall.exe" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \ + "DisplayIcon" "$INSTDIR\pixelorama.exe,0" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \ "InstallLocation" "$INSTDIR" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \ @@ -128,7 +141,19 @@ "NoModify" 1 WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \ "NoRepair" 1 + + ; Associate .pxo files with Pixelorama + WriteRegStr HKCR ".pxo" "" "Pixelorama project" + WriteRegStr HKCR ".pxo" "ContentType" "image/pixelorama" + WriteRegStr HKCR ".pxo" "PerceivedType" "document" + WriteRegStr HKCR "Pixelorama project" "" "Pixelorama project" + WriteRegStr HKCR "Pixelorama project\shell" "" "open" + WriteRegStr HKCR "Pixelorama project\DefaultIcon" "" "$INSTDIR\pxo.ico" + + WriteRegStr HKCR "Pixelorama project\shell\open\command" "" '$INSTDIR\${APPNAME}.exe "%1"' + WriteRegStr HKCR "Pixelorama project\shell\edit" "" "Edit project in ${APPNAME}" + WriteRegStr HKCR "Pixelorama project\shell\edit\command" "" '$INSTDIR\${APPNAME}.exe "%1"' SectionEnd @@ -137,8 +162,8 @@ ; Create folder in Start Menu\Programs and create shortcuts for app and uninstaller CreateDirectory "$SMPROGRAMS\${COMPANYNAME}" - CreateShortCut "$SMPROGRAMS\${COMPANYNAME}\Pixelorama ${APPVERSION}.lnk" "$INSTDIR\Pixelorama.exe" - CreateShortCut "$SMPROGRAMS\${COMPANYNAME}\Uninstall.lnk" "$INSTDIR\Uninstall.exe" + CreateShortCut "$SMPROGRAMS\${COMPANYNAME}\${APPNAME} ${APPVERSION}.lnk" "$INSTDIR\Pixelorama.exe" + CreateShortCut "$SMPROGRAMS\${COMPANYNAME}\Uninstall.lnk" "$INSTDIR\uninstall.exe" SectionEnd @@ -166,11 +191,12 @@ SectionIn RO ; Delete all files and folders created by the installer - Delete "$INSTDIR\Uninstall.exe" + Delete "$INSTDIR\uninstall.exe" Delete "$INSTDIR\Pixelorama.exe" Delete "$INSTDIR\Pixelorama.pck" - Delete "$INSTDIR\libgifexporter.windows.64.dll" - RMDir /r "$INSTDIR\pixelorama" + Delete "$INSTDIR\pxo.ico" + RMDir /r "$INSTDIR\pixelorama_data" + RMDir "$INSTDIR" ; Delete shortcuts RMDir /r "$SMPROGRAMS\${COMPANYNAME}" @@ -186,16 +212,27 @@ ; Delete the Add/Remove Programs entry DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" + ; Delete the .pxo file association + DeleteRegKey HKCR "Pixelorama project" + DeleteRegKey HKCR ".pxo" + SectionEnd Section "un.$(un.SecConfig)" un.SecConfig ; Configuration removal section ; Delete the application's settings file - Delete "$APPDATA\Godot\app_userdata\Pixelorama\cache.ini" + Delete "$APPDATA\Godot\app_userdata\${APPNAME}\cache.ini" SectionEnd +; Uninstaller functions + + Function un.onInit + !insertmacro MUI_UNGETLANGUAGE + + FunctionEnd + ; Section description language strings for multilingual support diff --git a/installer/utils/LICENSE b/installer/utils/LICENSE new file mode 100644 index 000000000..7a3b7c2f6 --- /dev/null +++ b/installer/utils/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/installer/utils/__pycache__/polib.cpython-38.pyc b/installer/utils/__pycache__/polib.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ccea29a60921e9feb8d8ab524ce9d8287a7fb133 GIT binary patch literal 47452 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\n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#. SecInstall +#: ../pixelorama.nsi:84 +msgid "Install ${APPNAME}" +msgstr "" + +#. SecStartmenu +#: ../pixelorama.nsi:85 +msgid "Create Start Menu shortcuts (optional)" +msgstr "" + +#. SecDesktop +#: ../pixelorama.nsi:86 +msgid "Create shortcut on Desktop (optional)" +msgstr "" + +#. un.SecUninstall +#: ../pixelorama.nsi:87 +msgid "Uninstall ${APPNAME} ${APPVERSION}" +msgstr "" + +#. un.SecConfig +#: ../pixelorama.nsi:88 +msgid "Remove configuration files (optional)" +msgstr "" + +#. DESC_SecInstall +#: ../pixelorama.nsi:202 +msgid "Installs ${APPNAME} ${APPVERSION}." +msgstr "" + +#. DESC_SecStartmenu +#: ../pixelorama.nsi:203 +msgid "Creates Start Menu shortcuts for ${APPNAME}." +msgstr "" + +#. DESC_SecDesktop +#: ../pixelorama.nsi:204 +msgid "Creates a Desktop shortcut for ${APPNAME}." +msgstr "" + +#. DESC_un.SecUninstall +#: ../pixelorama.nsi:205 +msgid "Uninstalls ${APPNAME} ${APPVERSION} and removes all shortcuts." +msgstr "" + +#. DESC_un.SecConfig +#: ../pixelorama.nsi:206 +msgid "Removes configuration files for ${APPNAME}." +msgstr "" diff --git a/installer/utils/nsi2pot.py b/installer/utils/nsi2pot.py new file mode 100644 index 000000000..b098663a5 --- /dev/null +++ b/installer/utils/nsi2pot.py @@ -0,0 +1,143 @@ +""" +nsi2pot.py: Create gettext POT template file from NSIS script + +Copyright (C) 2021 huskee +(Original author: Dan Chowdhury) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +import collections +import polib +import datetime +import os +from optparse import OptionParser + +parser = OptionParser() +parser.add_option("-i", "--input", dest="input", + help="Input NSIS script location", metavar="script.nsi" ) +parser.add_option("-o", "--output", dest="output", + help="POT file output location", default="installer.pot") +parser.add_option("-p", "--project", dest="project", + help="Project name to write to the pot file") +parser.add_option("-v", "--version", dest="version", + help="Version to write to the pot file") +parser.add_option("-l", "--lang", dest="lang", + help="NSIS script default language (default is English)", default="English" ) + +(options, args) = parser.parse_args() + +metadata = { + "Project-Id-Version" : (options.project + " " + options.version).strip(), + "Report-Msgid-Bugs-To" : "", + "POT-Creation-Date" : datetime.datetime.now().strftime('%Y-%m-%d %H:%M%z'), + "PO-Revision-Date" : "YEAR-MO-DA HO:MI+ZONE", + "Last-Translator" : "FULL NAME ", + "Language-Team" : "LANGUAGE ", + "Language" : "", + "MIME-Version" : "1.0", + "Content-Type" : "text/plain; charset=UTF-8", + "Content-Transfer-Encoding" : "8bit" +} + +NSIFilePath = options.input + +# Removes trailing \ which marks a new line +def removeEscapedNewLine(line): + newline = line.rstrip("\n") + newline = line.rstrip() + newlen = len(newline) + if newline.rfind("\\")+1 == len(newline): + return newline[:newlen-1] + return line + +# Open our source file +NSIWorkingFile = open(NSIFilePath,"r") +NSIWorkingFileDir,NSIFileName = os.path.split(NSIFilePath) +# Create our new .POT file, and give our metadata +poFile = polib.POFile() +poFile.metadata = metadata +# Create a cache of messageValues : [ [fileName1,lineNumber1], [fileName2,lineNumber2]... ] (The same message could appear on multiple lines) +LangStringCache = collections.OrderedDict() +# Create a cache of messageValues : [ label1, label2 ] (The same message could have multiple NSIS labels) +LangStringLabels = {} + +# What we're doing here is looping through each line of our .nsi till we find a LangString of the default language +# Then, we try and grab the line number, the label, and the text +# The text can be multiline, so we have to sometimes continue reading till we reach the end +line=NSIWorkingFile.readline() +lineNo = 1 +while line != '': + commands = line.split() + if len(commands) > 3: + if commands[0] == "LangString" and commands[2].upper() == ("${LANG_%s}"%options.lang).upper(): + label = commands[1] + value = "" + # Let's assume it's a one-liner + start = line.find('"') + 1 + if start: + end = line.find('"',start) + if end != -1: + value = line[start:end] + else: # Nope, multiline + line = removeEscapedNewLine(line) + # Keep reading till we reach the end + value = line[start:] + line = NSIWorkingFile.readline() + lineNo += 1 + while line != '': + line = removeEscapedNewLine(line) + end = line.find('"') + if end != -1: #If we found the closing character, append + value += line[:end].lstrip() + break + else: #If not, append and continue + value += line.lstrip() + line=NSIWorkingFile.readline() + lineNo += 1 + + # Remove whitespace and new lines + value = value.strip("\t\n") + value = polib.unescape ( value ) + if not value in LangStringCache: + LangStringCache[value] = [] + # Note down our file and line number + LangStringCache[value].append([options.input,lineNo]) + + if not value in LangStringLabels: + LangStringLabels[value] = [] + # Note down our label + LangStringLabels[value].append(label) + + line=NSIWorkingFile.readline() + lineNo += 1 + +# Now, we loop through our cache and build PO entries for each +# We use PO comment field to store our NSIS labels, so we can decode it back later +for msgid,lineOccurances in LangStringCache.items(): + entry = polib.POEntry( + msgid=msgid, + msgstr='', + occurrences=lineOccurances, + comment=(" ").join(LangStringLabels[msgid]) + ) + poFile.append(entry) + + +NSIWorkingFile.close() + +# Finally, let's generate our POT file +poFile.save(options.output) + +print ( "%s: pot file generated" %options.output ) diff --git a/installer/utils/po2nsi.py b/installer/utils/po2nsi.py new file mode 100644 index 000000000..00ba73418 --- /dev/null +++ b/installer/utils/po2nsi.py @@ -0,0 +1,182 @@ +""" +po2nsi.py: Create multilingual NSIS script based on gettext +PO files + +Copyright (C) 2021 huskee +(Original author: Dan Chowdhury) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +import collections +import os +import polib +from optparse import OptionParser + +parser = OptionParser() +parser.add_option("-i", "--input", dest="input", + help="NSIS script to be localized", metavar="script.nsi") +parser.add_option("-o", "--output", dest="output", + help="Localized script output location", metavar="script.nsi") +parser.add_option("-p", "--podir", dest="podir", + help="Directory containing PO files") +parser.add_option("-l", "--lang", dest="lang", + help="NSIS script default language (default is English)", default="English" ) +parser.add_option("-v", "--verbose", action="store_true", + dest="verbose", help="Verbose output") + +(options, args) = parser.parse_args() + + +# Define a dict to convert locale names to language names +localeToName = { + "af-ZA" : "Afrikaans", + "ar-SA" : "Arabic", + "ca-ES" : "Catalan", + "cs-CZ" : "Czech", + "da-DK" : "Danish", + "nl-NL" : "Dutch", + "en" : "English", + "eo-UY" : "Esperanto", + "fi-FI" : "Finnish", + "fr-FR" : "French", + "de-DE" : "German", + "el-GR" : "Greek", + "he-IL" : "Hebrew", + "hi-IN" : "Hindi", + "hu-HU" : "Hungarian", + "id-ID" : "Indonesian", + "it-IT" : "Italian", + "ja-JP" : "Japanese", + "ko-KR" : "Korean", + "lv-LV" : "Latvian", + "no-NO" : "Norwegian", + "pl-PL" : "Polish", + "pt-PT" : "Portuguese", + "pt-BR" : "PortugueseBR", + "ro-RO" : "Romanian", + "ru-RU" : "Russian", + "sr-SP" : "Serbian", + "zh-CN" : "SimpChinese", + "es-ES" : "Spanish", + "sv-SE" : "Swedish", + "zh-TW" : "TradChinese", + "tr-TR" : "Turkish", + "uk-UA" : "Ukrainian", + "vi-VN" : "Vietnamese", +} + +localeRTL = [ "ar-SA", "he-IL" ] + +def escapeNSIS(st): + return st.replace('\\', r'$\\')\ + .replace('\t', r'$\t')\ + .replace('\r', r'\r')\ + .replace('\n', r'\n')\ + .replace('\"', r'$\"')\ + .replace('$$\\', '$\\') + +translationCache = {} + +# The purpose of this loop is to go to the podir scanning for PO files for each locale name +# Once we've found a PO file, we use PO lib to read every translated entry +# Using this, for each each language, we store a dict of entries - { nsilabel (comment) : translation (msgstr) } +# For untranslated entries, we use msgid instead of msgstr (i.e. default English string) +for root,dirs,files in os.walk(options.podir): + for file in files: + filename,ext = os.path.splitext(file) + if ext == ".po": + # Valid locale filename (fr.po, de.po etc)? + if filename not in localeToName: + print("%s: invalid filename, must be xx-YY language code" %(filename)) + else: + if options.verbose: + print("Valid filename found") + language = localeToName[filename] + translationCache[language] = collections.OrderedDict() + # Let's add a default LANGUAGE_CODE LangString to be read + translationCache[language]["LANGUAGE_CODE"] = filename + if options.verbose: + print("Language: %s (%s)" %(language, translationCache[language]["LANGUAGE_CODE"])) + + # Are we RTL? Mark that down too as a LangString + if filename in localeRTL: + translationCache[language]["LANGUAGE_RTL"] = "1" + if options.verbose: + print("RTL language") + else: + if options.verbose: + print("Non RTL language") + + po = polib.pofile(os.path.join(root,file)) + for entry in po.translated_entries(): + # Loop through all our labels and add translation (each translation may have multiple labels) + for label in entry.comment.split(): + translationCache[language][label] = escapeNSIS(entry.msgstr) + if options.verbose: + print("msgstr added, " + translationCache[language][label]) + # For untranslated strings, let's add the English entry + for entry in po.untranslated_entries(): + for label in entry.comment.split(): + print("Warning: Label '%s' for language %s remains untranslated"%(label,language)) + translationCache[language][label] = escapeNSIS(entry.msgid) + if options.verbose: + print('\n') + + + + + +# Open our source NSI, dump it to a list and close it +NSISourceFile = open(options.input,"r") +if options.verbose: + print("Opened source file") +NSISourceLines = NSISourceFile.readlines() +if options.verbose: + print("Read source file lines") +NSISourceFile.close() +if options.verbose: + print("Closed source file") +NSINewLines = [] + + +# Here we scan for ";@INSERT_TRANSLATIONS@" in the NSIS, and add MUI_LANGUAGE macros and LangString's for translation languages +lineNo = 1 +print('\n') +for line in NSISourceLines: + x = line.find(";@INSERT_TRANSLATIONS@") + if x != -1: + if options.verbose: + print("INSERT_TRANSLATIONS found") + NSINewLines.append('\n') + for language,translations in translationCache.items(): + count = 0 + # if the language isn't the default, we add our MUI_LANGUAGE macro + if language.upper() != options.lang.upper(): + NSINewLines.append(' !insertmacro MUI_LANGUAGE "%s"\n'%language) + # For every translation we grabbed from the .po, let's add our LangString + for label,value in translations.items(): + NSINewLines.append(' LangString %s ${LANG_%s} "%s"\n' % (label,language,value)) + count += 1 + NSINewLines.append('\n') + print ("%i translations merged for language %s" %(count,language)) + else: + NSINewLines.append (line) + +# Finally, let's write our new .nsi to the desired target file +NSIWorkingFile = open(options.output,"w",encoding='utf-8') +NSIWorkingFile.writelines(NSINewLines) +NSIWorkingFile.close() + +print ("%s: NSIS script successfully localized" %options.output) diff --git a/installer/utils/polib.py b/installer/utils/polib.py new file mode 100644 index 000000000..445655a8b --- /dev/null +++ b/installer/utils/polib.py @@ -0,0 +1,1880 @@ +# -* coding: utf-8 -*- +# +# License: MIT (see LICENSE file provided) +# vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: + +""" +**polib** allows you to manipulate, create, modify gettext files (pot, po and +mo files). You can load existing files, iterate through it's entries, add, +modify entries, comments or metadata, etc. or create new po files from scratch. + +**polib** provides a simple and pythonic API via the :func:`~polib.pofile` and +:func:`~polib.mofile` convenience functions. +""" + +import array +import codecs +import os +import re +import struct +import sys +import textwrap + +try: + import io +except ImportError: + # replacement of io.open() for python < 2.6 + # we use codecs instead + class io(object): + @staticmethod + def open(fpath, mode='r', encoding=None): + return codecs.open(fpath, mode, encoding) + + +__author__ = 'David Jean Louis ' +__version__ = '1.1.0' +__all__ = ['pofile', 'POFile', 'POEntry', 'mofile', 'MOFile', 'MOEntry', + 'default_encoding', 'escape', 'unescape', 'detect_encoding', ] + + +# the default encoding to use when encoding cannot be detected +default_encoding = 'utf-8' + +# python 2/3 compatibility helpers {{{ + + +if sys.version_info[:2] < (3, 0): + PY3 = False + text_type = unicode + + def b(s): + return s + + def u(s): + return unicode(s, "unicode_escape") + +else: + PY3 = True + text_type = str + + def b(s): + return s.encode("latin-1") + + def u(s): + return s +# }}} +# _pofile_or_mofile {{{ + + +def _pofile_or_mofile(f, type, **kwargs): + """ + Internal function used by :func:`polib.pofile` and :func:`polib.mofile` to + honor the DRY concept. + """ + # get the file encoding + enc = kwargs.get('encoding') + if enc is None: + enc = detect_encoding(f, type == 'mofile') + + # parse the file + kls = type == 'pofile' and _POFileParser or _MOFileParser + parser = kls( + f, + encoding=enc, + check_for_duplicates=kwargs.get('check_for_duplicates', False), + klass=kwargs.get('klass') + ) + instance = parser.parse() + instance.wrapwidth = kwargs.get('wrapwidth', 78) + return instance +# }}} +# _is_file {{{ + + +def _is_file(filename_or_contents): + """ + Safely returns the value of os.path.exists(filename_or_contents). + + Arguments: + + ``filename_or_contents`` + either a filename, or a string holding the contents of some file. + In the latter case, this function will always return False. + """ + try: + return os.path.exists(filename_or_contents) + except (ValueError, UnicodeEncodeError): + return False +# }}} +# function pofile() {{{ + + +def pofile(pofile, **kwargs): + """ + Convenience function that parses the po or pot file ``pofile`` and returns + a :class:`~polib.POFile` instance. + + Arguments: + + ``pofile`` + string, full or relative path to the po/pot file or its content (data). + + ``wrapwidth`` + integer, the wrap width, only useful when the ``-w`` option was passed + to xgettext (optional, default: ``78``). + + ``encoding`` + string, the encoding to use (e.g. "utf-8") (default: ``None``, the + encoding will be auto-detected). + + ``check_for_duplicates`` + whether to check for duplicate entries when adding entries to the + file (optional, default: ``False``). + + ``klass`` + class which is used to instantiate the return value (optional, + default: ``None``, the return value with be a :class:`~polib.POFile` + instance). + """ + return _pofile_or_mofile(pofile, 'pofile', **kwargs) +# }}} +# function mofile() {{{ + + +def mofile(mofile, **kwargs): + """ + Convenience function that parses the mo file ``mofile`` and returns a + :class:`~polib.MOFile` instance. + + Arguments: + + ``mofile`` + string, full or relative path to the mo file or its content (data). + + ``wrapwidth`` + integer, the wrap width, only useful when the ``-w`` option was passed + to xgettext to generate the po file that was used to format the mo file + (optional, default: ``78``). + + ``encoding`` + string, the encoding to use (e.g. "utf-8") (default: ``None``, the + encoding will be auto-detected). + + ``check_for_duplicates`` + whether to check for duplicate entries when adding entries to the + file (optional, default: ``False``). + + ``klass`` + class which is used to instantiate the return value (optional, + default: ``None``, the return value with be a :class:`~polib.POFile` + instance). + """ + return _pofile_or_mofile(mofile, 'mofile', **kwargs) +# }}} +# function detect_encoding() {{{ + + +def detect_encoding(file, binary_mode=False): + """ + Try to detect the encoding used by the ``file``. The ``file`` argument can + be a PO or MO file path or a string containing the contents of the file. + If the encoding cannot be detected, the function will return the value of + ``default_encoding``. + + Arguments: + + ``file`` + string, full or relative path to the po/mo file or its content. + + ``binary_mode`` + boolean, set this to True if ``file`` is a mo file. + """ + PATTERN = r'"?Content-Type:.+? charset=([\w_\-:\.]+)' + rxt = re.compile(u(PATTERN)) + rxb = re.compile(b(PATTERN)) + + def charset_exists(charset): + """Check whether ``charset`` is valid or not.""" + try: + codecs.lookup(charset) + except LookupError: + return False + return True + + if not _is_file(file): + match = rxt.search(file) + if match: + enc = match.group(1).strip() + if charset_exists(enc): + return enc + else: + # For PY3, always treat as binary + if binary_mode or PY3: + mode = 'rb' + rx = rxb + else: + mode = 'r' + rx = rxt + f = open(file, mode) + for l in f.readlines(): + match = rx.search(l) + if match: + f.close() + enc = match.group(1).strip() + if not isinstance(enc, text_type): + enc = enc.decode('utf-8') + if charset_exists(enc): + return enc + f.close() + return default_encoding +# }}} +# function escape() {{{ + + +def escape(st): + """ + Escapes the characters ``\\\\``, ``\\t``, ``\\n``, ``\\r`` and ``"`` in + the given string ``st`` and returns it. + """ + return st.replace('\\', r'\\')\ + .replace('\t', r'\t')\ + .replace('\r', r'\r')\ + .replace('\n', r'\n')\ + .replace('\"', r'\"') +# }}} +# function unescape() {{{ + + +def unescape(st): + """ + Unescapes the characters ``\\\\``, ``\\t``, ``\\n``, ``\\r`` and ``"`` in + the given string ``st`` and returns it. + """ + def unescape_repl(m): + m = m.group(1) + if m == 'n': + return '\n' + if m == 't': + return '\t' + if m == 'r': + return '\r' + if m == '\\': + return '\\' + return m # handles escaped double quote + return re.sub(r'\\(\\|n|t|r|")', unescape_repl, st) +# }}} +# function natural_sort() {{{ + + +def natural_sort(lst): + """ + Sort naturally the given list. + Credits: http://stackoverflow.com/a/4836734 + """ + def convert(text): + return int(text) if text.isdigit() else text.lower() + + def alphanum_key(key): + return [convert(c) for c in re.split('([0-9]+)', key)] + + return sorted(lst, key=alphanum_key) + +# }}} +# class _BaseFile {{{ + + +class _BaseFile(list): + """ + Common base class for the :class:`~polib.POFile` and :class:`~polib.MOFile` + classes. This class should **not** be instantiated directly. + """ + + def __init__(self, *args, **kwargs): + """ + Constructor, accepts the following keyword arguments: + + ``pofile`` + string, the path to the po or mo file, or its content as a string. + + ``wrapwidth`` + integer, the wrap width, only useful when the ``-w`` option was + passed to xgettext (optional, default: ``78``). + + ``encoding`` + string, the encoding to use, defaults to ``default_encoding`` + global variable (optional). + + ``check_for_duplicates`` + whether to check for duplicate entries when adding entries to the + file, (optional, default: ``False``). + """ + list.__init__(self) + # the opened file handle + pofile = kwargs.get('pofile', None) + if pofile and _is_file(pofile): + self.fpath = pofile + else: + self.fpath = kwargs.get('fpath') + # the width at which lines should be wrapped + self.wrapwidth = kwargs.get('wrapwidth', 78) + # the file encoding + self.encoding = kwargs.get('encoding', default_encoding) + # whether to check for duplicate entries or not + self.check_for_duplicates = kwargs.get('check_for_duplicates', False) + # header + self.header = '' + # both po and mo files have metadata + self.metadata = {} + self.metadata_is_fuzzy = 0 + + def __unicode__(self): + """ + Returns the unicode representation of the file. + """ + ret = [] + entries = [self.metadata_as_entry()] + \ + [e for e in self if not e.obsolete] + for entry in entries: + ret.append(entry.__unicode__(self.wrapwidth)) + for entry in self.obsolete_entries(): + ret.append(entry.__unicode__(self.wrapwidth)) + ret = u('\n').join(ret) + return ret + + if PY3: + def __str__(self): + return self.__unicode__() + else: + def __str__(self): + """ + Returns the string representation of the file. + """ + return unicode(self).encode(self.encoding) + + def __contains__(self, entry): + """ + Overridden ``list`` method to implement the membership test (in and + not in). + The method considers that an entry is in the file if it finds an entry + that has the same msgid (the test is **case sensitive**) and the same + msgctxt (or none for both entries). + + Argument: + + ``entry`` + an instance of :class:`~polib._BaseEntry`. + """ + return self.find(entry.msgid, by='msgid', msgctxt=entry.msgctxt) \ + is not None + + def __eq__(self, other): + return str(self) == str(other) + + def append(self, entry): + """ + Overridden method to check for duplicates entries, if a user tries to + add an entry that is already in the file, the method will raise a + ``ValueError`` exception. + + Argument: + + ``entry`` + an instance of :class:`~polib._BaseEntry`. + """ + # check_for_duplicates may not be defined (yet) when unpickling. + # But if pickling, we never want to check for duplicates anyway. + if getattr(self, 'check_for_duplicates', False) and entry in self: + raise ValueError('Entry "%s" already exists' % entry.msgid) + super(_BaseFile, self).append(entry) + + def insert(self, index, entry): + """ + Overridden method to check for duplicates entries, if a user tries to + add an entry that is already in the file, the method will raise a + ``ValueError`` exception. + + Arguments: + + ``index`` + index at which the entry should be inserted. + + ``entry`` + an instance of :class:`~polib._BaseEntry`. + """ + if self.check_for_duplicates and entry in self: + raise ValueError('Entry "%s" already exists' % entry.msgid) + super(_BaseFile, self).insert(index, entry) + + def metadata_as_entry(self): + """ + Returns the file metadata as a :class:`~polib.POFile` instance. + """ + e = POEntry(msgid='') + mdata = self.ordered_metadata() + if mdata: + strs = [] + for name, value in mdata: + # Strip whitespace off each line in a multi-line entry + strs.append('%s: %s' % (name, value)) + e.msgstr = '\n'.join(strs) + '\n' + if self.metadata_is_fuzzy: + e.flags.append('fuzzy') + return e + + def save(self, fpath=None, repr_method='__unicode__'): + """ + Saves the po file to ``fpath``. + If it is an existing file and no ``fpath`` is provided, then the + existing file is rewritten with the modified data. + + Keyword arguments: + + ``fpath`` + string, full or relative path to the file. + + ``repr_method`` + string, the method to use for output. + """ + if self.fpath is None and fpath is None: + raise IOError('You must provide a file path to save() method') + contents = getattr(self, repr_method)() + if fpath is None: + fpath = self.fpath + if repr_method == 'to_binary': + fhandle = open(fpath, 'wb') + else: + fhandle = io.open(fpath, 'w', encoding=self.encoding) + if not isinstance(contents, text_type): + contents = contents.decode(self.encoding) + fhandle.write(contents) + fhandle.close() + # set the file path if not set + if self.fpath is None and fpath: + self.fpath = fpath + + def find(self, st, by='msgid', include_obsolete_entries=False, + msgctxt=False): + """ + Find the entry which msgid (or property identified by the ``by`` + argument) matches the string ``st``. + + Keyword arguments: + + ``st`` + string, the string to search for. + + ``by`` + string, the property to use for comparison (default: ``msgid``). + + ``include_obsolete_entries`` + boolean, whether to also search in entries that are obsolete. + + ``msgctxt`` + string, allows specifying a specific message context for the + search. + """ + if include_obsolete_entries: + entries = self[:] + else: + entries = [e for e in self if not e.obsolete] + matches = [] + for e in entries: + if getattr(e, by) == st: + if msgctxt is not False and e.msgctxt != msgctxt: + continue + matches.append(e) + if len(matches) == 1: + return matches[0] + elif len(matches) > 1: + if not msgctxt: + # find the entry with no msgctx + e = None + for m in matches: + if not m.msgctxt: + e = m + if e: + return e + # fallback to the first entry found + return matches[0] + return None + + def ordered_metadata(self): + """ + Convenience method that returns an ordered version of the metadata + dictionary. The return value is list of tuples (metadata name, + metadata_value). + """ + # copy the dict first + metadata = self.metadata.copy() + data_order = [ + 'Project-Id-Version', + 'Report-Msgid-Bugs-To', + 'POT-Creation-Date', + 'PO-Revision-Date', + 'Last-Translator', + 'Language-Team', + 'Language', + 'MIME-Version', + 'Content-Type', + 'Content-Transfer-Encoding', + 'Plural-Forms' + ] + ordered_data = [] + for data in data_order: + try: + value = metadata.pop(data) + ordered_data.append((data, value)) + except KeyError: + pass + # the rest of the metadata will be alphabetically ordered since there + # are no specs for this AFAIK + for data in natural_sort(metadata.keys()): + value = metadata[data] + ordered_data.append((data, value)) + return ordered_data + + def to_binary(self): + """ + Return the binary representation of the file. + """ + offsets = [] + entries = self.translated_entries() + + # the keys are sorted in the .mo file + def cmp(_self, other): + # msgfmt compares entries with msgctxt if it exists + self_msgid = _self.msgctxt and _self.msgctxt or _self.msgid + other_msgid = other.msgctxt and other.msgctxt or other.msgid + if self_msgid > other_msgid: + return 1 + elif self_msgid < other_msgid: + return -1 + else: + return 0 + # add metadata entry + entries.sort(key=lambda o: o.msgid_with_context.encode('utf-8')) + mentry = self.metadata_as_entry() + entries = [mentry] + entries + entries_len = len(entries) + ids, strs = b(''), b('') + for e in entries: + # For each string, we need size and file offset. Each string is + # NUL terminated; the NUL does not count into the size. + msgid = b('') + if e.msgctxt: + # Contexts are stored by storing the concatenation of the + # context, a byte, and the original string + msgid = self._encode(e.msgctxt + '\4') + if e.msgid_plural: + msgstr = [] + for index in sorted(e.msgstr_plural.keys()): + msgstr.append(e.msgstr_plural[index]) + msgid += self._encode(e.msgid + '\0' + e.msgid_plural) + msgstr = self._encode('\0'.join(msgstr)) + else: + msgid += self._encode(e.msgid) + msgstr = self._encode(e.msgstr) + offsets.append((len(ids), len(msgid), len(strs), len(msgstr))) + ids += msgid + b('\0') + strs += msgstr + b('\0') + + # The header is 7 32-bit unsigned integers. + keystart = 7 * 4 + 16 * entries_len + # and the values start after the keys + valuestart = keystart + len(ids) + koffsets = [] + voffsets = [] + # The string table first has the list of keys, then the list of values. + # Each entry has first the size of the string, then the file offset. + for o1, l1, o2, l2 in offsets: + koffsets += [l1, o1 + keystart] + voffsets += [l2, o2 + valuestart] + offsets = koffsets + voffsets + + output = struct.pack( + "Iiiiiii", + # Magic number + MOFile.MAGIC, + # Version + 0, + # number of entries + entries_len, + # start of key index + 7 * 4, + # start of value index + 7 * 4 + entries_len * 8, + # size and offset of hash table, we don't use hash tables + 0, keystart + + ) + if PY3 and sys.version_info.minor > 1: # python 3.2 or superior + output += array.array("i", offsets).tobytes() + else: + output += array.array("i", offsets).tostring() + output += ids + output += strs + return output + + def _encode(self, mixed): + """ + Encodes the given ``mixed`` argument with the file encoding if and + only if it's an unicode string and returns the encoded string. + """ + if isinstance(mixed, text_type): + mixed = mixed.encode(self.encoding) + return mixed +# }}} +# class POFile {{{ + + +class POFile(_BaseFile): + """ + Po (or Pot) file reader/writer. + This class inherits the :class:`~polib._BaseFile` class and, by extension, + the python ``list`` type. + """ + + def __unicode__(self): + """ + Returns the unicode representation of the po file. + """ + ret, headers = '', self.header.split('\n') + for header in headers: + if not len(header): + ret += "#\n" + elif header[:1] in [',', ':']: + ret += '#%s\n' % header + else: + ret += '# %s\n' % header + + if not isinstance(ret, text_type): + ret = ret.decode(self.encoding) + + return ret + _BaseFile.__unicode__(self) + + def save_as_mofile(self, fpath): + """ + Saves the binary representation of the file to given ``fpath``. + + Keyword argument: + + ``fpath`` + string, full or relative path to the mo file. + """ + _BaseFile.save(self, fpath, 'to_binary') + + def percent_translated(self): + """ + Convenience method that returns the percentage of translated + messages. + """ + total = len([e for e in self if not e.obsolete]) + if total == 0: + return 100 + translated = len(self.translated_entries()) + return int(translated * 100 / float(total)) + + def translated_entries(self): + """ + Convenience method that returns the list of translated entries. + """ + return [e for e in self if e.translated()] + + def untranslated_entries(self): + """ + Convenience method that returns the list of untranslated entries. + """ + return [e for e in self if not e.translated() and not e.obsolete + and not e.fuzzy] + + def fuzzy_entries(self): + """ + Convenience method that returns the list of fuzzy entries. + """ + return [e for e in self if e.fuzzy] + + def obsolete_entries(self): + """ + Convenience method that returns the list of obsolete entries. + """ + return [e for e in self if e.obsolete] + + def merge(self, refpot): + """ + Convenience method that merges the current pofile with the pot file + provided. It behaves exactly as the gettext msgmerge utility: + + * comments of this file will be preserved, but extracted comments and + occurrences will be discarded; + * any translations or comments in the file will be discarded, however, + dot comments and file positions will be preserved; + * the fuzzy flags are preserved. + + Keyword argument: + + ``refpot`` + object POFile, the reference catalog. + """ + # Store entries in dict/set for faster access + self_entries = dict( + (entry.msgid_with_context, entry) for entry in self + ) + refpot_msgids = set(entry.msgid_with_context for entry in refpot) + # Merge entries that are in the refpot + for entry in refpot: + e = self_entries.get(entry.msgid_with_context) + if e is None: + e = POEntry() + self.append(e) + e.merge(entry) + # ok, now we must "obsolete" entries that are not in the refpot anymore + for entry in self: + if entry.msgid_with_context not in refpot_msgids: + entry.obsolete = True +# }}} +# class MOFile {{{ + + +class MOFile(_BaseFile): + """ + Mo file reader/writer. + This class inherits the :class:`~polib._BaseFile` class and, by + extension, the python ``list`` type. + """ + MAGIC = 0x950412de + MAGIC_SWAPPED = 0xde120495 + + def __init__(self, *args, **kwargs): + """ + Constructor, accepts all keywords arguments accepted by + :class:`~polib._BaseFile` class. + """ + _BaseFile.__init__(self, *args, **kwargs) + self.magic_number = None + self.version = 0 + + def save_as_pofile(self, fpath): + """ + Saves the mofile as a pofile to ``fpath``. + + Keyword argument: + + ``fpath`` + string, full or relative path to the file. + """ + _BaseFile.save(self, fpath) + + def save(self, fpath=None): + """ + Saves the mofile to ``fpath``. + + Keyword argument: + + ``fpath`` + string, full or relative path to the file. + """ + _BaseFile.save(self, fpath, 'to_binary') + + def percent_translated(self): + """ + Convenience method to keep the same interface with POFile instances. + """ + return 100 + + def translated_entries(self): + """ + Convenience method to keep the same interface with POFile instances. + """ + return self + + def untranslated_entries(self): + """ + Convenience method to keep the same interface with POFile instances. + """ + return [] + + def fuzzy_entries(self): + """ + Convenience method to keep the same interface with POFile instances. + """ + return [] + + def obsolete_entries(self): + """ + Convenience method to keep the same interface with POFile instances. + """ + return [] +# }}} +# class _BaseEntry {{{ + + +class _BaseEntry(object): + """ + Base class for :class:`~polib.POEntry` and :class:`~polib.MOEntry` classes. + This class should **not** be instantiated directly. + """ + + def __init__(self, *args, **kwargs): + """ + Constructor, accepts the following keyword arguments: + + ``msgid`` + string, the entry msgid. + + ``msgstr`` + string, the entry msgstr. + + ``msgid_plural`` + string, the entry msgid_plural. + + ``msgstr_plural`` + list, the entry msgstr_plural lines. + + ``msgctxt`` + string, the entry context (msgctxt). + + ``obsolete`` + bool, whether the entry is "obsolete" or not. + + ``encoding`` + string, the encoding to use, defaults to ``default_encoding`` + global variable (optional). + """ + self.msgid = kwargs.get('msgid', '') + self.msgstr = kwargs.get('msgstr', '') + self.msgid_plural = kwargs.get('msgid_plural', '') + self.msgstr_plural = kwargs.get('msgstr_plural', {}) + self.msgctxt = kwargs.get('msgctxt', None) + self.obsolete = kwargs.get('obsolete', False) + self.encoding = kwargs.get('encoding', default_encoding) + + def __unicode__(self, wrapwidth=78): + """ + Returns the unicode representation of the entry. + """ + if self.obsolete: + delflag = '#~ ' + else: + delflag = '' + ret = [] + # write the msgctxt if any + if self.msgctxt is not None: + ret += self._str_field("msgctxt", delflag, "", self.msgctxt, + wrapwidth) + # write the msgid + ret += self._str_field("msgid", delflag, "", self.msgid, wrapwidth) + # write the msgid_plural if any + if self.msgid_plural: + ret += self._str_field("msgid_plural", delflag, "", + self.msgid_plural, wrapwidth) + if self.msgstr_plural: + # write the msgstr_plural if any + msgstrs = self.msgstr_plural + keys = list(msgstrs) + keys.sort() + for index in keys: + msgstr = msgstrs[index] + plural_index = '[%s]' % index + ret += self._str_field("msgstr", delflag, plural_index, msgstr, + wrapwidth) + else: + # otherwise write the msgstr + ret += self._str_field("msgstr", delflag, "", self.msgstr, + wrapwidth) + ret.append('') + ret = u('\n').join(ret) + return ret + + if PY3: + def __str__(self): + return self.__unicode__() + else: + def __str__(self): + """ + Returns the string representation of the entry. + """ + return unicode(self).encode(self.encoding) + + def __eq__(self, other): + return str(self) == str(other) + + def _str_field(self, fieldname, delflag, plural_index, field, + wrapwidth=78): + lines = field.splitlines(True) + if len(lines) > 1: + lines = [''] + lines # start with initial empty line + else: + escaped_field = escape(field) + specialchars_count = 0 + for c in ['\\', '\n', '\r', '\t', '"']: + specialchars_count += field.count(c) + # comparison must take into account fieldname length + one space + # + 2 quotes (eg. msgid "") + flength = len(fieldname) + 3 + if plural_index: + flength += len(plural_index) + real_wrapwidth = wrapwidth - flength + specialchars_count + if wrapwidth > 0 and len(field) > real_wrapwidth: + # Wrap the line but take field name into account + lines = [''] + [unescape(item) for item in wrap( + escaped_field, + wrapwidth - 2, # 2 for quotes "" + drop_whitespace=False, + break_long_words=False + )] + else: + lines = [field] + if fieldname.startswith('previous_'): + # quick and dirty trick to get the real field name + fieldname = fieldname[9:] + + ret = ['%s%s%s "%s"' % (delflag, fieldname, plural_index, + escape(lines.pop(0)))] + for line in lines: + ret.append('%s"%s"' % (delflag, escape(line))) + return ret +# }}} +# class POEntry {{{ + + +class POEntry(_BaseEntry): + """ + Represents a po file entry. + """ + + def __init__(self, *args, **kwargs): + """ + Constructor, accepts the following keyword arguments: + + ``comment`` + string, the entry comment. + + ``tcomment`` + string, the entry translator comment. + + ``occurrences`` + list, the entry occurrences. + + ``flags`` + list, the entry flags. + + ``previous_msgctxt`` + string, the entry previous context. + + ``previous_msgid`` + string, the entry previous msgid. + + ``previous_msgid_plural`` + string, the entry previous msgid_plural. + + ``linenum`` + integer, the line number of the entry + """ + _BaseEntry.__init__(self, *args, **kwargs) + self.comment = kwargs.get('comment', '') + self.tcomment = kwargs.get('tcomment', '') + self.occurrences = kwargs.get('occurrences', []) + self.flags = kwargs.get('flags', []) + self.previous_msgctxt = kwargs.get('previous_msgctxt', None) + self.previous_msgid = kwargs.get('previous_msgid', None) + self.previous_msgid_plural = kwargs.get('previous_msgid_plural', None) + self.linenum = kwargs.get('linenum', None) + + def __unicode__(self, wrapwidth=78): + """ + Returns the unicode representation of the entry. + """ + ret = [] + # comments first, if any (with text wrapping as xgettext does) + if self.obsolete: + comments = [('tcomment', '# ')] + else: + comments = [('comment', '#. '), ('tcomment', '# ')] + for c in comments: + val = getattr(self, c[0]) + if val: + for comment in val.split('\n'): + if wrapwidth > 0 and len(comment) + len(c[1]) > wrapwidth: + ret += wrap( + comment, + wrapwidth, + initial_indent=c[1], + subsequent_indent=c[1], + break_long_words=False + ) + else: + ret.append('%s%s' % (c[1], comment)) + + # occurrences (with text wrapping as xgettext does) + if not self.obsolete and self.occurrences: + filelist = [] + for fpath, lineno in self.occurrences: + if lineno: + filelist.append('%s:%s' % (fpath, lineno)) + else: + filelist.append(fpath) + filestr = ' '.join(filelist) + if wrapwidth > 0 and len(filestr) + 3 > wrapwidth: + # textwrap split words that contain hyphen, this is not + # what we want for filenames, so the dirty hack is to + # temporally replace hyphens with a char that a file cannot + # contain, like "*" + ret += [l.replace('*', '-') for l in wrap( + filestr.replace('-', '*'), + wrapwidth, + initial_indent='#: ', + subsequent_indent='#: ', + break_long_words=False + )] + else: + ret.append('#: ' + filestr) + + # flags (TODO: wrapping ?) + if self.flags: + ret.append('#, %s' % ', '.join(self.flags)) + + # previous context and previous msgid/msgid_plural + fields = ['previous_msgctxt', 'previous_msgid', + 'previous_msgid_plural'] + if self.obsolete: + prefix = "#~| " + else: + prefix = "#| " + for f in fields: + val = getattr(self, f) + if val: + ret += self._str_field(f, prefix, "", val, wrapwidth) + + ret.append(_BaseEntry.__unicode__(self, wrapwidth)) + ret = u('\n').join(ret) + return ret + + def __cmp__(self, other): + """ + Called by comparison operations if rich comparison is not defined. + """ + # First: Obsolete test + if self.obsolete != other.obsolete: + if self.obsolete: + return -1 + else: + return 1 + # Work on a copy to protect original + occ1 = sorted(self.occurrences[:]) + occ2 = sorted(other.occurrences[:]) + pos = 0 + if occ1 > occ2: + return 1 + if occ1 < occ2: + return -1 + # Compare context + msgctxt = self.msgctxt or 0 + othermsgctxt = other.msgctxt or 0 + if msgctxt > othermsgctxt: + return 1 + elif msgctxt < othermsgctxt: + return -1 + # Compare msgid_plural + msgid_plural = self.msgid_plural or 0 + othermsgid_plural = other.msgid_plural or 0 + if msgid_plural > othermsgid_plural: + return 1 + elif msgid_plural < othermsgid_plural: + return -1 + # Compare msgstr_plural + msgstr_plural = self.msgstr_plural or 0 + othermsgstr_plural = other.msgstr_plural or 0 + if msgstr_plural > othermsgstr_plural: + return 1 + elif msgstr_plural < othermsgstr_plural: + return -1 + # Compare msgid + if self.msgid > other.msgid: + return 1 + elif self.msgid < other.msgid: + return -1 + return 0 + # Compare msgstr + if self.msgstr > other.msgstr: + return 1 + elif self.msgstr < other.msgstr: + return -1 + return 0 + + def __gt__(self, other): + return self.__cmp__(other) > 0 + + def __lt__(self, other): + return self.__cmp__(other) < 0 + + def __ge__(self, other): + return self.__cmp__(other) >= 0 + + def __le__(self, other): + return self.__cmp__(other) <= 0 + + def __eq__(self, other): + return self.__cmp__(other) == 0 + + def __ne__(self, other): + return self.__cmp__(other) != 0 + + def translated(self): + """ + Returns ``True`` if the entry has been translated or ``False`` + otherwise. + """ + if self.obsolete or self.fuzzy: + return False + if self.msgstr != '': + return True + if self.msgstr_plural: + for pos in self.msgstr_plural: + if self.msgstr_plural[pos] == '': + return False + return True + return False + + def merge(self, other): + """ + Merge the current entry with the given pot entry. + """ + self.msgid = other.msgid + self.msgctxt = other.msgctxt + self.occurrences = other.occurrences + self.comment = other.comment + fuzzy = self.fuzzy + self.flags = other.flags[:] # clone flags + if fuzzy: + self.flags.append('fuzzy') + self.msgid_plural = other.msgid_plural + self.obsolete = other.obsolete + self.previous_msgctxt = other.previous_msgctxt + self.previous_msgid = other.previous_msgid + self.previous_msgid_plural = other.previous_msgid_plural + if other.msgstr_plural: + for pos in other.msgstr_plural: + try: + # keep existing translation at pos if any + self.msgstr_plural[pos] + except KeyError: + self.msgstr_plural[pos] = '' + + @property + def fuzzy(self): + return 'fuzzy' in self.flags + + @property + def msgid_with_context(self): + if self.msgctxt: + return '%s%s%s' % (self.msgctxt, "\x04", self.msgid) + return self.msgid + + def __hash__(self): + return hash((self.msgid, self.msgstr)) +# }}} +# class MOEntry {{{ + + +class MOEntry(_BaseEntry): + """ + Represents a mo file entry. + """ + def __init__(self, *args, **kwargs): + """ + Constructor, accepts the following keyword arguments, + for consistency with :class:`~polib.POEntry`: + + ``comment`` + ``tcomment`` + ``occurrences`` + ``flags`` + ``previous_msgctxt`` + ``previous_msgid`` + ``previous_msgid_plural`` + + Note: even though these keyword arguments are accepted, + they hold no real meaning in the context of MO files + and are simply ignored. + """ + _BaseEntry.__init__(self, *args, **kwargs) + self.comment = '' + self.tcomment = '' + self.occurrences = [] + self.flags = [] + self.previous_msgctxt = None + self.previous_msgid = None + self.previous_msgid_plural = None + + def __hash__(self): + return hash((self.msgid, self.msgstr)) + +# }}} +# class _POFileParser {{{ + + +class _POFileParser(object): + """ + A finite state machine to parse efficiently and correctly po + file format. + """ + + def __init__(self, pofile, *args, **kwargs): + """ + Constructor. + + Keyword arguments: + + ``pofile`` + string, path to the po file or its content + + ``encoding`` + string, the encoding to use, defaults to ``default_encoding`` + global variable (optional). + + ``check_for_duplicates`` + whether to check for duplicate entries when adding entries to the + file (optional, default: ``False``). + """ + enc = kwargs.get('encoding', default_encoding) + if _is_file(pofile): + try: + self.fhandle = io.open(pofile, 'rt', encoding=enc) + except LookupError: + enc = default_encoding + self.fhandle = io.open(pofile, 'rt', encoding=enc) + else: + self.fhandle = pofile.splitlines() + + klass = kwargs.get('klass') + if klass is None: + klass = POFile + self.instance = klass( + pofile=pofile, + encoding=enc, + check_for_duplicates=kwargs.get('check_for_duplicates', False) + ) + self.transitions = {} + self.current_line = 0 + self.current_entry = POEntry(linenum=self.current_line) + self.current_state = 'st' + self.current_token = None + # two memo flags used in handlers + self.msgstr_index = 0 + self.entry_obsolete = 0 + # Configure the state machine, by adding transitions. + # Signification of symbols: + # * ST: Beginning of the file (start) + # * HE: Header + # * TC: a translation comment + # * GC: a generated comment + # * OC: a file/line occurrence + # * FL: a flags line + # * CT: a message context + # * PC: a previous msgctxt + # * PM: a previous msgid + # * PP: a previous msgid_plural + # * MI: a msgid + # * MP: a msgid plural + # * MS: a msgstr + # * MX: a msgstr plural + # * MC: a msgid or msgstr continuation line + all = ['st', 'he', 'gc', 'oc', 'fl', 'ct', 'pc', 'pm', 'pp', 'tc', + 'ms', 'mp', 'mx', 'mi'] + + self.add('tc', ['st', 'he'], 'he') + self.add('tc', ['gc', 'oc', 'fl', 'tc', 'pc', 'pm', 'pp', 'ms', + 'mp', 'mx', 'mi'], 'tc') + self.add('gc', all, 'gc') + self.add('oc', all, 'oc') + self.add('fl', all, 'fl') + self.add('pc', all, 'pc') + self.add('pm', all, 'pm') + self.add('pp', all, 'pp') + self.add('ct', ['st', 'he', 'gc', 'oc', 'fl', 'tc', 'pc', 'pm', + 'pp', 'ms', 'mx'], 'ct') + self.add('mi', ['st', 'he', 'gc', 'oc', 'fl', 'ct', 'tc', 'pc', + 'pm', 'pp', 'ms', 'mx'], 'mi') + self.add('mp', ['tc', 'gc', 'pc', 'pm', 'pp', 'mi'], 'mp') + self.add('ms', ['mi', 'mp', 'tc'], 'ms') + self.add('mx', ['mi', 'mx', 'mp', 'tc'], 'mx') + self.add('mc', ['ct', 'mi', 'mp', 'ms', 'mx', 'pm', 'pp', 'pc'], 'mc') + + def parse(self): + """ + Run the state machine, parse the file line by line and call process() + with the current matched symbol. + """ + + keywords = { + 'msgctxt': 'ct', + 'msgid': 'mi', + 'msgstr': 'ms', + 'msgid_plural': 'mp', + } + prev_keywords = { + 'msgid_plural': 'pp', + 'msgid': 'pm', + 'msgctxt': 'pc', + } + tokens = [] + fpath = '%s ' % self.instance.fpath if self.instance.fpath else '' + for line in self.fhandle: + self.current_line += 1 + line = line.strip() + if line == '': + continue + + tokens = line.split(None, 2) + nb_tokens = len(tokens) + + if tokens[0] == '#~|': + continue + + if tokens[0] == '#~' and nb_tokens > 1: + line = line[3:].strip() + tokens = tokens[1:] + nb_tokens -= 1 + self.entry_obsolete = 1 + else: + self.entry_obsolete = 0 + + # Take care of keywords like + # msgid, msgid_plural, msgctxt & msgstr. + if tokens[0] in keywords and nb_tokens > 1: + line = line[len(tokens[0]):].lstrip() + if re.search(r'([^\\]|^)"', line[1:-1]): + raise IOError('Syntax error in po file %s(line %s): ' + 'unescaped double quote found' % + (fpath, self.current_line)) + self.current_token = line + self.process(keywords[tokens[0]]) + continue + + self.current_token = line + + if tokens[0] == '#:': + if nb_tokens <= 1: + continue + # we are on a occurrences line + self.process('oc') + + elif line[:1] == '"': + # we are on a continuation line + if re.search(r'([^\\]|^)"', line[1:-1]): + raise IOError('Syntax error in po file %s(line %s): ' + 'unescaped double quote found' % + (fpath, self.current_line)) + self.process('mc') + + elif line[:7] == 'msgstr[': + # we are on a msgstr plural + self.process('mx') + + elif tokens[0] == '#,': + if nb_tokens <= 1: + continue + # we are on a flags line + self.process('fl') + + elif tokens[0] == '#' or tokens[0].startswith('##'): + if line == '#': + line += ' ' + # we are on a translator comment line + self.process('tc') + + elif tokens[0] == '#.': + if nb_tokens <= 1: + continue + # we are on a generated comment line + self.process('gc') + + elif tokens[0] == '#|': + if nb_tokens <= 1: + raise IOError('Syntax error in po file %s(line %s)' % + (fpath, self.current_line)) + + # Remove the marker and any whitespace right after that. + line = line[2:].lstrip() + self.current_token = line + + if tokens[1].startswith('"'): + # Continuation of previous metadata. + self.process('mc') + continue + + if nb_tokens == 2: + # Invalid continuation line. + raise IOError('Syntax error in po file %s(line %s): ' + 'invalid continuation line' % + (fpath, self.current_line)) + + # we are on a "previous translation" comment line, + if tokens[1] not in prev_keywords: + # Unknown keyword in previous translation comment. + raise IOError('Syntax error in po file %s(line %s): ' + 'unknown keyword %s' % + (fpath, self.current_line, + tokens[1])) + + # Remove the keyword and any whitespace + # between it and the starting quote. + line = line[len(tokens[1]):].lstrip() + self.current_token = line + self.process(prev_keywords[tokens[1]]) + + else: + raise IOError('Syntax error in po file %s(line %s)' % + (fpath, self.current_line)) + + if self.current_entry and len(tokens) > 0 and \ + not tokens[0].startswith('#'): + # since entries are added when another entry is found, we must add + # the last entry here (only if there are lines). Trailing comments + # are ignored + self.instance.append(self.current_entry) + + # before returning the instance, check if there's metadata and if + # so extract it in a dict + metadataentry = self.instance.find('') + if metadataentry: # metadata found + # remove the entry + self.instance.remove(metadataentry) + self.instance.metadata_is_fuzzy = metadataentry.flags + key = None + for msg in metadataentry.msgstr.splitlines(): + try: + key, val = msg.split(':', 1) + self.instance.metadata[key] = val.strip() + except (ValueError, KeyError): + if key is not None: + self.instance.metadata[key] += '\n' + msg.strip() + # close opened file + if not isinstance(self.fhandle, list): # must be file + self.fhandle.close() + return self.instance + + def add(self, symbol, states, next_state): + """ + Add a transition to the state machine. + + Keywords arguments: + + ``symbol`` + string, the matched token (two chars symbol). + + ``states`` + list, a list of states (two chars symbols). + + ``next_state`` + the next state the fsm will have after the action. + """ + for state in states: + action = getattr(self, 'handle_%s' % next_state) + self.transitions[(symbol, state)] = (action, next_state) + + def process(self, symbol): + """ + Process the transition corresponding to the current state and the + symbol provided. + + Keywords arguments: + + ``symbol`` + string, the matched token (two chars symbol). + + ``linenum`` + integer, the current line number of the parsed file. + """ + try: + (action, state) = self.transitions[(symbol, self.current_state)] + if action(): + self.current_state = state + except Exception: + raise IOError('Syntax error in po file (line %s)' % + self.current_line) + + # state handlers + + def handle_he(self): + """Handle a header comment.""" + if self.instance.header != '': + self.instance.header += '\n' + self.instance.header += self.current_token[2:] + return 1 + + def handle_tc(self): + """Handle a translator comment.""" + if self.current_state in ['mc', 'ms', 'mx']: + self.instance.append(self.current_entry) + self.current_entry = POEntry(linenum=self.current_line) + if self.current_entry.tcomment != '': + self.current_entry.tcomment += '\n' + tcomment = self.current_token.lstrip('#') + if tcomment.startswith(' '): + tcomment = tcomment[1:] + self.current_entry.tcomment += tcomment + return True + + def handle_gc(self): + """Handle a generated comment.""" + if self.current_state in ['mc', 'ms', 'mx']: + self.instance.append(self.current_entry) + self.current_entry = POEntry(linenum=self.current_line) + if self.current_entry.comment != '': + self.current_entry.comment += '\n' + self.current_entry.comment += self.current_token[3:] + return True + + def handle_oc(self): + """Handle a file:num occurrence.""" + if self.current_state in ['mc', 'ms', 'mx']: + self.instance.append(self.current_entry) + self.current_entry = POEntry(linenum=self.current_line) + occurrences = self.current_token[3:].split() + for occurrence in occurrences: + if occurrence != '': + try: + fil, line = occurrence.rsplit(':', 1) + if not line.isdigit(): + fil = occurrence + line = '' + self.current_entry.occurrences.append((fil, line)) + except (ValueError, AttributeError): + self.current_entry.occurrences.append((occurrence, '')) + return True + + def handle_fl(self): + """Handle a flags line.""" + if self.current_state in ['mc', 'ms', 'mx']: + self.instance.append(self.current_entry) + self.current_entry = POEntry(linenum=self.current_line) + self.current_entry.flags += [c.strip() for c in + self.current_token[3:].split(',')] + return True + + def handle_pp(self): + """Handle a previous msgid_plural line.""" + if self.current_state in ['mc', 'ms', 'mx']: + self.instance.append(self.current_entry) + self.current_entry = POEntry(linenum=self.current_line) + self.current_entry.previous_msgid_plural = \ + unescape(self.current_token[1:-1]) + return True + + def handle_pm(self): + """Handle a previous msgid line.""" + if self.current_state in ['mc', 'ms', 'mx']: + self.instance.append(self.current_entry) + self.current_entry = POEntry(linenum=self.current_line) + self.current_entry.previous_msgid = \ + unescape(self.current_token[1:-1]) + return True + + def handle_pc(self): + """Handle a previous msgctxt line.""" + if self.current_state in ['mc', 'ms', 'mx']: + self.instance.append(self.current_entry) + self.current_entry = POEntry(linenum=self.current_line) + self.current_entry.previous_msgctxt = \ + unescape(self.current_token[1:-1]) + return True + + def handle_ct(self): + """Handle a msgctxt.""" + if self.current_state in ['mc', 'ms', 'mx']: + self.instance.append(self.current_entry) + self.current_entry = POEntry(linenum=self.current_line) + self.current_entry.msgctxt = unescape(self.current_token[1:-1]) + return True + + def handle_mi(self): + """Handle a msgid.""" + if self.current_state in ['mc', 'ms', 'mx']: + self.instance.append(self.current_entry) + self.current_entry = POEntry(linenum=self.current_line) + self.current_entry.obsolete = self.entry_obsolete + self.current_entry.msgid = unescape(self.current_token[1:-1]) + return True + + def handle_mp(self): + """Handle a msgid plural.""" + self.current_entry.msgid_plural = unescape(self.current_token[1:-1]) + return True + + def handle_ms(self): + """Handle a msgstr.""" + self.current_entry.msgstr = unescape(self.current_token[1:-1]) + return True + + def handle_mx(self): + """Handle a msgstr plural.""" + index = self.current_token[7] + value = self.current_token[self.current_token.find('"') + 1:-1] + self.current_entry.msgstr_plural[int(index)] = unescape(value) + self.msgstr_index = int(index) + return True + + def handle_mc(self): + """Handle a msgid or msgstr continuation line.""" + token = unescape(self.current_token[1:-1]) + if self.current_state == 'ct': + self.current_entry.msgctxt += token + elif self.current_state == 'mi': + self.current_entry.msgid += token + elif self.current_state == 'mp': + self.current_entry.msgid_plural += token + elif self.current_state == 'ms': + self.current_entry.msgstr += token + elif self.current_state == 'mx': + self.current_entry.msgstr_plural[self.msgstr_index] += token + elif self.current_state == 'pp': + self.current_entry.previous_msgid_plural += token + elif self.current_state == 'pm': + self.current_entry.previous_msgid += token + elif self.current_state == 'pc': + self.current_entry.previous_msgctxt += token + # don't change the current state + return False +# }}} +# class _MOFileParser {{{ + + +class _MOFileParser(object): + """ + A class to parse binary mo files. + """ + + def __init__(self, mofile, *args, **kwargs): + """ + Constructor. + + Keyword arguments: + + ``mofile`` + string, path to the mo file or its content + + ``encoding`` + string, the encoding to use, defaults to ``default_encoding`` + global variable (optional). + + ``check_for_duplicates`` + whether to check for duplicate entries when adding entries to the + file (optional, default: ``False``). + """ + self.fhandle = open(mofile, 'rb') + + klass = kwargs.get('klass') + if klass is None: + klass = MOFile + self.instance = klass( + fpath=mofile, + encoding=kwargs.get('encoding', default_encoding), + check_for_duplicates=kwargs.get('check_for_duplicates', False) + ) + + def __del__(self): + """ + Make sure the file is closed, this prevents warnings on unclosed file + when running tests with python >= 3.2. + """ + if self.fhandle: + self.fhandle.close() + + def parse(self): + """ + Build the instance with the file handle provided in the + constructor. + """ + # parse magic number + magic_number = self._readbinary('> 16 not in (0, 1): + raise IOError('Invalid mo file, unexpected major revision number') + self.instance.version = version + # original strings and translation strings hash table offset + msgids_hash_offset, msgstrs_hash_offset = self._readbinary(ii, 8) + # move to msgid hash table and read length and offset of msgids + self.fhandle.seek(msgids_hash_offset) + msgids_index = [] + for i in range(numofstrings): + msgids_index.append(self._readbinary(ii, 8)) + # move to msgstr hash table and read length and offset of msgstrs + self.fhandle.seek(msgstrs_hash_offset) + msgstrs_index = [] + for i in range(numofstrings): + msgstrs_index.append(self._readbinary(ii, 8)) + # build entries + encoding = self.instance.encoding + for i in range(numofstrings): + self.fhandle.seek(msgids_index[i][1]) + msgid = self.fhandle.read(msgids_index[i][0]) + + self.fhandle.seek(msgstrs_index[i][1]) + msgstr = self.fhandle.read(msgstrs_index[i][0]) + if i == 0 and not msgid: # metadata + raw_metadata, metadata = msgstr.split(b('\n')), {} + for line in raw_metadata: + tokens = line.split(b(':'), 1) + if tokens[0] != b(''): + try: + k = tokens[0].decode(encoding) + v = tokens[1].decode(encoding) + metadata[k] = v.strip() + except IndexError: + metadata[k] = u('') + self.instance.metadata = metadata + continue + # test if we have a plural entry + msgid_tokens = msgid.split(b('\0')) + if len(msgid_tokens) > 1: + entry = self._build_entry( + msgid=msgid_tokens[0], + msgid_plural=msgid_tokens[1], + msgstr_plural=dict((k, v) for k, v in + enumerate(msgstr.split(b('\0')))) + ) + else: + entry = self._build_entry(msgid=msgid, msgstr=msgstr) + self.instance.append(entry) + # close opened file + self.fhandle.close() + return self.instance + + def _build_entry(self, msgid, msgstr=None, msgid_plural=None, + msgstr_plural=None): + msgctxt_msgid = msgid.split(b('\x04')) + encoding = self.instance.encoding + if len(msgctxt_msgid) > 1: + kwargs = { + 'msgctxt': msgctxt_msgid[0].decode(encoding), + 'msgid': msgctxt_msgid[1].decode(encoding), + } + else: + kwargs = {'msgid': msgid.decode(encoding)} + if msgstr: + kwargs['msgstr'] = msgstr.decode(encoding) + if msgid_plural: + kwargs['msgid_plural'] = msgid_plural.decode(encoding) + if msgstr_plural: + for k in msgstr_plural: + msgstr_plural[k] = msgstr_plural[k].decode(encoding) + kwargs['msgstr_plural'] = msgstr_plural + return MOEntry(**kwargs) + + def _readbinary(self, fmt, numbytes): + """ + Private method that unpack n bytes of data using format . + It returns a tuple or a mixed value if the tuple length is 1. + """ + bytes = self.fhandle.read(numbytes) + tup = struct.unpack(fmt, bytes) + if len(tup) == 1: + return tup[0] + return tup +# }}} +# class TextWrapper {{{ + + +class TextWrapper(textwrap.TextWrapper): + """ + Subclass of textwrap.TextWrapper that backport the + drop_whitespace option. + """ + def __init__(self, *args, **kwargs): + drop_whitespace = kwargs.pop('drop_whitespace', True) + textwrap.TextWrapper.__init__(self, *args, **kwargs) + self.drop_whitespace = drop_whitespace + + def _wrap_chunks(self, chunks): + """_wrap_chunks(chunks : [string]) -> [string] + + Wrap a sequence of text chunks and return a list of lines of + length 'self.width' or less. (If 'break_long_words' is false, + some lines may be longer than this.) Chunks correspond roughly + to words and the whitespace between them: each chunk is + indivisible (modulo 'break_long_words'), but a line break can + come between any two chunks. Chunks should not have internal + whitespace; ie. a chunk is either all whitespace or a "word". + Whitespace chunks will be removed from the beginning and end of + lines, but apart from that whitespace is preserved. + """ + lines = [] + if self.width <= 0: + raise ValueError("invalid width %r (must be > 0)" % self.width) + + # Arrange in reverse order so items can be efficiently popped + # from a stack of chucks. + chunks.reverse() + + while chunks: + + # Start the list of chunks that will make up the current line. + # cur_len is just the length of all the chunks in cur_line. + cur_line = [] + cur_len = 0 + + # Figure out which static string will prefix this line. + if lines: + indent = self.subsequent_indent + else: + indent = self.initial_indent + + # Maximum width for this line. + width = self.width - len(indent) + + # First chunk on line is whitespace -- drop it, unless this + # is the very beginning of the text (ie. no lines started yet). + if self.drop_whitespace and chunks[-1].strip() == '' and lines: + del chunks[-1] + + while chunks: + length = len(chunks[-1]) + + # Can at least squeeze this chunk onto the current line. + if cur_len + length <= width: + cur_line.append(chunks.pop()) + cur_len += length + + # Nope, this line is full. + else: + break + + # The current line is full, and the next chunk is too big to + # fit on *any* line (not just this one). + if chunks and len(chunks[-1]) > width: + self._handle_long_word(chunks, cur_line, cur_len, width) + + # If the last chunk on this line is all whitespace, drop it. + if self.drop_whitespace and cur_line and not cur_line[-1].strip(): + del cur_line[-1] + + # Convert current line back to a string and store it in list + # of all lines (return value). + if cur_line: + lines.append(indent + ''.join(cur_line)) + + return lines +# }}} +# function wrap() {{{ + + +def wrap(text, width=70, **kwargs): + """ + Wrap a single paragraph of text, returning a list of wrapped lines. + """ + if sys.version_info < (2, 6): + return TextWrapper(width=width, **kwargs).wrap(text) + return textwrap.wrap(text, width=width, **kwargs) + +# }}}