From 5ff7b6008e3b9946dc61065b578ebe0e3996486c Mon Sep 17 00:00:00 2001 From: "Rodolfo M. Raya" Date: Wed, 17 Jul 2019 09:46:50 -0300 Subject: [PATCH] Used constants to indicate success or error --- lib/xlifffilters.jar | Bin 516471 -> 517126 bytes src/com/maxprograms/converters/Constants.java | 6 +- src/com/maxprograms/converters/Convert.java | 803 +++-- src/com/maxprograms/converters/Merge.java | 1135 +++--- .../maxprograms/converters/TmxExporter.java | 919 ++--- .../converters/ditamap/DitaMap2Xliff.java | 1327 +++---- .../converters/ditamap/Xliff2DitaMap.java | 503 +-- .../converters/html/Html2Xliff.java | 2111 +++++------ .../converters/html/Xliff2Html.java | 520 +-- .../converters/idml/Idml2Xliff.java | 575 +-- .../converters/idml/Story2Xliff.java | 1163 +++--- .../converters/idml/Xliff2Idml.java | 381 +- .../javaproperties/Properties2Xliff.java | 411 +-- .../javaproperties/Xliff2Properties.java | 413 +-- .../converters/javascript/Jscript2xliff.java | 489 +-- .../converters/javascript/Xliff2jscript.java | 289 +- .../maxprograms/converters/mif/Mif2Xliff.java | 765 ++-- .../maxprograms/converters/mif/Xliff2Mif.java | 431 +-- .../converters/msoffice/MSOffice2Xliff.java | 5 +- .../converters/office/Office2Xliff.java | 791 ++--- .../converters/office/Xliff2Office.java | 447 +-- .../converters/plaintext/Text2Xliff.java | 377 +- .../converters/plaintext/Xliff2Text.java | 364 +- .../maxprograms/converters/po/Po2Xliff.java | 977 +++--- .../maxprograms/converters/po/Xliff2Po.java | 753 ++-- .../maxprograms/converters/rc/Rc2Xliff.java | 1349 +++---- .../maxprograms/converters/rc/Xliff2Rc.java | 535 +-- .../converters/resx/Resx2Xliff.java | 297 +- .../converters/resx/Xliff2Resx.java | 203 +- .../converters/sdlxliff/Sdl2Xliff.java | 549 +-- .../converters/sdlxliff/Xliff2Sdl.java | 493 +-- .../maxprograms/converters/ts/Ts2Xliff.java | 325 +- .../maxprograms/converters/ts/Xliff2Ts.java | 325 +- .../converters/txml/Txml2Xliff.java | 397 +-- .../converters/txml/Xliff2Txml.java | 321 +- .../maxprograms/converters/xml/Xliff2Xml.java | 1127 +++--- .../maxprograms/converters/xml/Xml2Xliff.java | 3117 +++++++++-------- src/com/maxprograms/server/FilterServer.java | 1269 +++---- src/com/maxprograms/xliff2/FromXliff2.java | 7 +- src/com/maxprograms/xliff2/ToXliff2.java | 7 +- 40 files changed, 13193 insertions(+), 13083 deletions(-) diff --git a/lib/xlifffilters.jar b/lib/xlifffilters.jar index be2eaf876154b9b1dbbfdea253fb633cdd8a90a5..153743bbde1b1d312f00f18d499ffdf3e2ec6936 100644 GIT binary patch delta 252603 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%0DS9U6!Qq^JYuI#$R!wbLs)vQC&j2@h3 zU(n3$jHZ0t?%;)Yh7(M?%Jf#g5K3?&P`nt=d3cU#q9vJ#6($ODq&oTrOBtV;b(@2@ z?Nu<8<>_EgSyB-nktRoKQ<$5M7USz%o&PldN)$A7%Y>pHzT;XhOl zx&Xfe$))nhRzs+}%BP-cC}pVv>Z^v)AT^xw)Cd};M$&Y31R~obX@MGrSodf|KSi`m zjiIP2q0=#LjhaB0sfl#0nnX9K$#jc4mNu#>bf+q%d(?5XMNOp_RT;garqR1V`S*eE z9|76_rcOY_bt3J5R+HGJ7P75Q;_m8X?x_}WmO6#|s#AH83Ui)X%)?YUPgfN@Q$=`z zs^pVZ6)#rHc$uoZ}&;LB7DzN!(vY6ah-R`N!*itkjb`5tvTZ&7E!XPwEf zsI%a+&VkQ5mp@YH@!!-1{GGaxcdJI_QkTMiT?YSkIXu^Y6)HUuR_-K4^5t*S!b2DM(Dp*E=V)kbxpx(!rr$EZ8h4Qdm5{1&ZE>S1-K zdQ9D=o>QCEAJx6;6?LC_M?IiEL~!;6#(l50soiS3R%)mAsE4&rJ)%?9qdKG>)5Fx` z`Y82;9;=>z(o@yb`b71Nu2#?LTJ=Z0TD_ppP%r80)XVw?^{U>WUekA}*Y#%ghTf{) z(hsV)^%J1?jCxOhpgz!_s=w&3)rYRW>Lb@E^|5QT`qVW>ec(DyecpDy;VO&!B-=Gk zH5eb=9lc}Xqt$MnW%4Dh6(48vC6}H`D@~;A)*haJY%X3r`Xj0|>9$vYO*2eJWEsER zK*N4UxWX9&QtYl-o&4q`imlX#yiRt+WKK7ZY(?mks7hSGvz^)9(<(D{=)0G4G3N=$ zr5M?~eBbLhSC`NEwnp8U{vk4^tWYPpO6b5`W!j-~l_AMoMS}4>Sa=yQ!=W;~$hA{7 z&dAZ{oK)t9FJ1Iu2{!>h0JX z^k%3F1ms%Zm*PdH_^K3h461dB7cW^$K`CBlYU|_0OKzeFC~~3e4mzpBELsrN?Dq=% z2E8t)TScPws{P}0bLSKECcBMHY>sv(RSnZpYBAb@9i*hU?P=kJFr}5PM^La zt-eOk_zewJ-_ktw9f0@;s#5=g(ftSz{x_YgexfVXZn{nFp-pNp?NC3{V`?8gsaolO zElu=ilz-5gbF_;G!$jw3FQ0+u+1ldsw2yDpDZEkJ{G3kZmvvYEM5pnWx*PvXrz^M4 zP#L`xUJ9YSm$LEOJ1?+@!hyZ={S6ZI6+h8%5WAwo4QcW? zTR#M{axt?$eo|ewQ{6IOK5UEPCr(=gl(mzDIoFtk%+_h9DU?k^?L$;~Mp#HuV^<<%6J~IsXx2au4DZ0>> z@843S(VJK*2g`nAM=K|(&5k|6(_Lmp(}V+2dlCh-tDTjQp@v@<6#i>5o_I1QsB!r}M$4{vThw&6Wg=gtf zUZ{`bMS3bP(`9@u%IoxWV<(%026X ztSyCppkC&S1{f4E$y!<#53pPEvJ=6Q);JwU8I0|gmN%rp(HrnrJ>f_WMiOpYqV1Dj-1>3NpBp=1UqT0&o@gpE7SRW(bQDv^vziUQ= z$!g-?MrF3e^@*-c`if9yrlf8|Gt|4r`cmVmqx;k`S(zItCF^GLX5K zKR+7E%yJI?-9dMM{NUdc@E|WbL$Zx`jVG)}QlJa1#2o-kOD6u;TrFQZ6|M1eC^O(U zR{$x26z2-SN?rk2L0=%nTme{e1&|WI0`LaBas}X%q{E$Qo^k~+CaXB;Msg&_haVX8 z?TM0O$f+oOZ1y!1>^(wB#&yb!_FdClbQ<<%Qqp{ zkal!Ka`{&%gELL3qBzTW8kyuLGO;2VoJ_?2D+1NYT{-FAcbyKF&LWCWOA*jT&eeg|vwm&&ppf|zUWP_xd zy`;&VSi3uazV-vyPG2V3PJhA1zc3p=<)9l+96ewio=%o<5aH4VZL^$i5?xn3fO5yv zTvwKpEf-*Hmgqdx5#w^&ca{@d$%2O_#IGDPtQXP*eKJkfi)gMsg~B>awR$nF z))jQVuB6L#6|K{?v{Bd59l9Pa;xzibj?#AB09O&CM|2}St5?uVdL_N5SJTJ(boxf0 zLEoYMNBt|>r_W}eK8I8E`P@xkzyq~jAzKl=Nm-7;R1=r{+IjXPX z)Acofe3rhJFVffXHTrtKMc=@8=o@*nzKOT!oB3IN3%{ms<@fbE{#38$uk;4~R^Ntj z?GBZuH>uwGx9Tw6qz3Cd)f8lebM@V-4q4$1`d;;0eZP7@Z$XCn05Z#FWRY9do4Q54 zueYgB^mg@y-l4wLJJk>RVYN>`rhWQxouQw9&^`5&x{rQJ57tlXBlIp^te?>n^t1Xn z{hXempVxEsAN2zLlCIaU=vDeveXf2@U!-5xcj&kEX8n%7U%#if=|6$m`=Ih?jQR_D zo~+;0AL$SE$HqI9omfjWr$nShT-GrTg zZpdn)xv%rkMp|a>>%4RaEj9Ob7TrsW%zd4Yw$p5LU*|{6=iJw&sJZA@BLOJFh2iQ; zQ#zOWslS_#YQ(W!Nx@c{Y(CXXq?x^Ke=8kBDaopXUFY?R=ckE2M6J{>UYEQ(41we*w+-k}~z*sgM4e2J3GqUw=zS>F;Q?{(&axe^RObH_g(! zX`cR>PS*RVT({Ci;LfjIif%>Soh}zW=yKC8m)GcB$YEXl+(R+}=MHcIO*f|)ipr_T z=!tXlmqjUisT+ilihsK7B^O$MhCtAP-Q>+Czx;nt;$j(uu|l&p?$Hw;S?EFl<}v2b z@Vi2{b9gA%KEi#st0Yk|&{U-8k~J>$?~=Wi{CNSlxcD_L4!E*5wmzC?+lndeS6+SCC3wz3F)L zo9}Xxyb@;zaqd{O#8?p{LrQ#H(Und?f|`vv)9F~lF&ExO8jd;lH96#MrGWF9D~yAU zdGz8QyP?Fpsh9lCWAzWoAl0`>aph}yMGw(^>U)p;kKP_qKa30hKTt~t2tl5a0*oyH z06JCx08mQ<1QY-W2nYZIlWmuPdjUm%e3a#x|38zN`MylPgfKDasK||BAcR8^6AmGO z1Sf#u40te^d?X{2nK(xX)l#iTRjl>Ct9ZtG19cM2TCKKf)wSEM)>`d$+tszZtM1mG zQr7hUe&5Vw0u$i(%a89pp7(j5^Zz{W@Wxlq{{X-lvQL9U;19(*YC3}5o$*+ITRhm2 zs0qcQU1mIK#uGK|$&N_P#z=VU)>+Fr)Zi49Uli;L)%)uQr6qsO2&ENXgIk*P*gQ#y}(%?Yc)M6!YBh?R0u`{ z!clW&s-wk>uT>%_6URcq$ogPBtmc_TXR}d3ir<+A$}!TzQJsN#Dgum1EZDj<6PP*}Dtb#Wv~4RRS;JY} z&yQ^n7GjS2r{2I4EESYe{nf!Z1KUo}@~SCAlA%t=+2oi?nDObYVOqHX=V(|l)CxNi zNwdR10F8nY#V{W0WKium#I9LI4qs9dtunA0=Q85W?r=h30V95YliFBgV6C;$v5gWW z)Y5tb8?cc^PsY|YHRMShHIp@5A;V2LPs8S{>!VahS)^ulFxj3^u0LNev1;g{ERTFz zy8J@Hq@f$ZB*kt?B~8mji?TBhf(U77IqXtJ#Y{blq_B z5>-wZ4$nN;im(cQvWo;GtA={o4zP%U5*RALq6T8Q09%uRXbl$&eA#mi(H!R{GuUdz zHPCU#=d;Zt@gR;l#ppt}hV6oJIq5^~!FYmBSd$D!TZ8e|dV5Zj?ZDUQ<#lV9T{t%f z)A=Vdl=OEeE>(JcT`+4!CvsX@_y-ecfie3u0~k!nd~ZWvY7&`?l4!TknyE5Iw+9uFr?dqc5#P_cPPT~z3# zjz?J1lyoX$dkkpMHGFRvh|QFI9Z%47$zYrS<-uck%D~fjhNj@WzMUo_SQoVCk=h6y zd&o2qOT|N`j(y}t-^62GW~+|p1deD-UDa!#5Br&aal9&*zUx&q2<8^J^O!JGM?YP( z!%Qf9y%LTB;vO&w}^=n1MM{ z)!o+&ysmC93bs=3HwHVpu{z!&TM6+#d{ilk%d$1^0n>p*mk>o{}S)Be(}mRDcamjBuu*SgnYeo#7t8i zA5h#G9{d`=(eQ8VoCfLYBf$j0=HHbJ8hx}f*kP(l_d5gsfsYu>Tnoh7+RXSdB(5U&5FT9u)Ai>qK$dG|Ia3W zOC9UB_B$1dvwvM}ed}YLy*Jg7)>XP6lzV^nj%P)S9SxwpvB23AXKi`ccgdDzT7rG`2|$r8zbM6z$#s3l|%eMIc&%swP0A6t*k#AGoKZEFm6 zS_gIU2|z5AlMFdoM%!^fUnMPlyjA(Ve}Mg8U5Sj5ahi;!-G}ZNGF~PSD_Ae+zSOR|4k^2nwlpV!p zN16?kLDyxos`X0$9V)5LH{=4j(AM1QW%!^QeBTt%d@xSb@!7iY(IS(7b!k;8VO77! zWUK008g*&smJ&cNGGv=X7~K})%`MPj9MdD3VcTN+QZTll)`Q+wKSH#|LX3(do1bFG zX-|Ia-;o^QIgBR9iiYJ(zT}O%}d+o$OgvFD#cBa;fk^;#LoiMM-<7 zp7in!cJgu=W2LJ8Nd5YMMERy6SE$aaL?wDK98H{K?oh0+Qg>a=XjQGXb=ftBTr1Zp zcsHYM$#x5a8q^Oz7Qp$2y~V0HYlCbAWD^fr?+T&8rCv3*p;$x}PgkNd7&6s0w;6J~ z++laki+3ay2#qQfW}6qYhm*Suxm)hxd8@rw;%;87e#Ua!PXm>IclQ}`zwBmj%$Jx2 zY&%ryM!HmVjHwFbHvvPpRl~va530OEWm)*1AI%J|jGA zjH8L_R5YB_S`2sjgFfNwdbn7rQ+eG5E8Y)tC^R8lq7)BdW1fHgad%N0-%8 zVH_LrA^>jIL>K>mRke|i7_Js`?BL9!p5fMdX=cAHZ+`^m++d*|CLEBmpqxw;pNYMnl3 z8jEXPKG$m)saCzq`mkK>a4C(IccWN+n>Hvq&MMS*T_2i@Ucd!?*s`G?Tbq5(UbOGS zwmx)7ty^lfesnhXBi=l%7sxs z)26b-v73w3{5~}tIJGA;^A)G@FU_@{*S`8>FSe!euj=%f)@j~W>mFq0bH7-t`P{F; zmnfbxY{2(Ko@h%RW z#j~|cn0ZW&`Fx@-fFFxETFjk#tiTei#Zqj>GHk)wi1M-5#nnp@z?EpktyqQKSdE9! zgy*n^b+8F9@Wks_k2kRazeO`X#rgO%E}%?*7xKO>q9G_=w8%JwWF}f=0ZgeU1_sb3 z>(DNn5SDGYND|m4U5Lm|MCEeCu>2AKG4h#f10pu}unAs}>Qqy;z2JL2NVMej6-TNE(8&c0NaVq(Ob zGEzJWxa1=g+aucHH&P-7O33RiQGjL84o^uLN2RpHjWR;YQAS(L#}gclphZr@9*)W> z`>EvUMG-X^EfsPcoKi_?M=8v*WFL`#lYJCBAcbeYMCAaT=GHJ#!zmi3Sbq!o2b3N^ zK$I&vd`rW-)?cTFom>Q?Wtt4&>lk@dTFLG`X8#dUzl0~kLv(+|0q6(d93XNQAAu9d z6dT}(3nNJ18VzEr_z=$;}sjsWgfkvp#PEzIU(Rr%ZZKC_RA@Mf|Wfu zzk0t+6ucyUm1O&68tc+-jPp4a>bMj>Oto>&=OTL8_z=htmb-UjbUummC7F?ySs5yF zK3#D4!0xGiGWS_tZ>1DwS=H!f&TgUHx3c=&#;S8WEpP{gzY{ZX7Zd3oCd<9(;9eK* zw?c4%t%But#cMkTxT5Mn|DX5e@RMLAr#P_XG2<>(iq z$O?tyQS=oml(}V$CX)_gwkG}qm_$p?qks315?v-Zeu?ta%aOn**L92Hl^jfmDQH%Yn?p=NT+RlW$L|uvXQk)wkUg6 zNYJu)g@n>#(wr62mVssoZ&0T!!@ja7XVmE|@*v9Sco>yJ+ zl4$i54f+HP_e{>9Iy$98qMTI{ZDPXU;@J!2Vw%js(*cQF(Q_J>NP?zyl8gTkgW6Qd z994>Wr)2V}$fSuhS)xgQlZtoCu)yg|dA>pI;B<75sRHWLl;*wC6)2kTFQ1o|?F;tg z@dnMB;allW%TCLLm*0iGB$~V%yLi~Sg~!V$?8Z)id7a{Qr={KKGvq!D|OPBPPM4Gv$;~Q)E>@TSaB3q{-f-BGMuiY)m=>BZ#^(j!rtjdl~qMn z0NhM>1AFPveGJ&|v*mh@4oG7Rdb!hw$=J{AdLA|C$4tCPzq~|@`ytEL%UF&du}u9K z=i)U6$m_VArD+#`-oj0I8&Bh>*n^+()Xz!%E_Z*y-S;^A72d}O_z=Iwmn>rk@jLP1 zBRLr#%LG0&YVdn2$j-BUE4!>j(PbLJ_j-EXBYyd|+`#Y|DHEiWkY5C!oFw1j$jLzZ zJ2O-L_vb?LyisoA>0K;yH;ej-z8?$Z7LIf)%&K7wP&Jo-1(a6^%MrAL49|lo7yTe> z>OnFt{}N-y9DrLj#Jj1yoy|@P@;9s9RWYtQ*)p~-{=FcI-I|s=5AAzDWefdTUf)|L z_hwq^*f0pOjx&v`+p`F<7js{QhgZx^%Xj&yPRj!}QV<{5{5@GAkMiG@)fMs>(ZU90 zg%_?YlHk#Q9*p(5a>(IUeWJTU9w$`z+!jE%G63=6AVAcz>j(Zo@xI{bk4*Q!u+01w z<0aE zCjIjyVZ%v|r(1~ZVuLhNp5m@M2OBn3GDnr7&y=J%<56@Fl?06elDG%KV-P?1#9)I% z*RbVN8Q>+i&(R=z-0D~I&t>mfzJCBvO9u#2aQLr_5dZ*vw|b2MItBuyZI^(10WFu{ zz5xZ73&#f#m-v$b6@MTiBmoj5iZVeE5HS)S5)6cgGD&WdfqBGvK(J~B>jUvwP!|-d z6-#Za^|4N(QE_*zblbISyS2NuyKcAL?sm7@)o#14u5I?5duNiFFo|jL_Y3pA&Ue1^ zeq6ruhZkP~Z~+}tVI$ObM8oyrKwobx+8qmoF|x zc1HqLs_hy^3vji>G`W=!s2d_}n3=TUl1o7~kl~3p+vfPsNz$;^IaXzKp7}K;5W7u@hAUN1yQE##BsGaR$Mg zhawHraV9g^ZLp4KRX2}Te1?|nEfr_wn21G?6K4~g?SFxeonk5SYOxE1Y7Gt)sHi2J zFc#8n!av6^3t2X+~k2E~zBDdR~f4{NPL$yyET za0Nl_3B=c!kFq~+cA`a)?=(V2A{r61xl)4*ZhsXU37Uy27_Gn5ly@uI#N|08l;sm( zt3YhkuuX!3)+rc`wUHHl!Gwyd*&Ie>aBZ-+esi$5Dbkxv@NFXyRuLdnWIva6=*D)m zxp5Ub<$0EXFa11&W`U(C=+@AKAfbp=zAnJ}PFP}_oDp&Q0XC(b%z83z#OgYO>;qv$ zRewZs{cBG=VT3jG;u_0_*2SW|9IN(>^4MG(%H9%+;u;bn`2wSl%_lBfyEN=Jx57L5 zZF6gnhHLRT_Ps>(%JofI^hS(CJ?}88>+yLNH)O&|gc%V>s9zUI^u#5c+&E&|nM)ZQ z*|6+2`v_;}9t09haCM8cKHkrsWpgY(uvh|yN>c#=wCB}0%p`<$348&^o<6b%aB0(QH%|>8C z9m4%8?qhd9g_0GKM2z+K0RD{qi_K(BAZ(0w>a;;PafDCmYFju4?76zGc559w?|;_1 z_|A|UGw_gxqj;E+gae6=9wR=6tFhZY!m%TB>&qG*!!fqdP#~UYigX%%YrDp{*(C5^ z(Qq7J<)R^;Y>%4<62qFB@{p_IYZ=GP+8)juGw=-!PvR-2cvm2lH00e+%Voo!@q~Ew z6L?0$vv`huA<)}vL^$irn~-azX@4;BJg*@ovUet;)}c7mfQA=jvzQgKv^^dTv3-d- zi7~Xv&r2F!#w+Y`yJH;7nT6^t%UB4l5^7%6@E7D$l~i|)2k{CDtOjzodZ&X#0vZ_J3trKs{{^EV7^nt#|-yrJQH z_*)LnY&{G?b#uiGSvD@f@jv{zYQZ&%^+m@@BOe_*V_T zz`sdEye1h4jkfX(SDVE#Uw;#Aye7`qCO^N@@GjmH(=`&SjYLnB<*8IRSx)?48s5hT zoP1bzlHyFak%1&h>;9E;ktAUMpmtm&f48bbokz1s1XS;Nh#!-!m*iA+rT+VTXb5fX!HJVH%EN#Z$9PREl zViWLAGiT1Uz#CoUr81RDGx7UWkeSWKE+eE-IaRROR;*vYc0HGYVu=;g#F+HA3iQM*oPjBWEz^sXN*M#~}%3L(A5y*}CdDQ&r zw~AU4+%>Nb^qK^!w2JL|m|c#54b;wcA+foxnox+IP=6TbFd;6(sWi^7IEh1$y`k=0 z?q?~e(GKFSMC0r>ToV&!Pb@#QN=w`(csR&J#YCk@i7 z^TY7x0e{-d4DeC!yzW>u+3TcB1@{*;x>;JdlI|?Iwo1Y3q+9vP&UDyu(tc)EuFI_h z8r_kp=r?e7b<$m|Dz3SmbPwAO?-;R2AT&=RkdwH1PY$(jB|OtCAb8;A(Ir(ULs+QG1<|A({Mb|m zF@FY8(bmsFs@7AP!jxArV|A@(rZrnLh^c%w>p9FGL=Dg8_!V85o5EbbLwBTbj=w-J z@GMB-Lcgl3FJLjj?>vqY-T4A8A-sx(a|dv_tkw{Y4E?Isv))4NcRh;@hv6MWYn$%s z$0om9cc-w0{BH6W4&th|L2Nfq+WXPr*M9_Vo`4nUMJaUoC+U+;qC|E*I|dMv6W&T8 zb_6aNlN$w&S6&(W25_DIW$YcmO&iHyO#aD(*v~H!sC9E;z2yo_AF-WUW=Ile7zsv6p*J=_;aD9l>BAnFMl^* zC{3ZiA20eVN)){0^!tvZNUzX+_A)(%L0!{*DV%6qpdTH2SFd;-3-yW={^~dy{AIFq znoGUxguh%bI|;X5?k^Mhmg(h>^NNQQUgzVqp7>Ikr!tGQ6#ix-Z_Uh4@tYRKX`mm} zdVY{|YyG%-k~Mtsi;*!GD?!;B9}o=N-M=p2AO4`1ui3>g9TgFjbPmzw4UJ zv=n~%^dJtk>E3?)hrdFvnCwj9KlKt}{MY37kzW^67?%OI5`U#7-YOAqiC$$do07tB z^h&)dh5v3_Fh#`6ul`oA6dALje<(8c$qb>9iUuKPsBx+tkc_vyOGn12V6vG2s# zntyXd^FGn1*xLa9S1x1$%dsxV(#K*MTaJ^*^NHsV1ME0nGVR7R8gJTY$}O%cgsXDB z!d~XHjHbdg8lP}gE)12YjiywVPIIZ3o?vq*6^6?ErNU6DUM39jAPkkBFvTv`Es`(Q zOEZ!$H6?Eu@b(NX?KuWql7BYf--s!j64r+$oU!5$bFKKpPq$)uY73hLDQ$kw$xo7E zEDKFz5md!lwo`n2Z$3CvET9KIZXJyUXt4Fn`qkiv7Dh-fe%|{x+c0 zC~K9iJXR??m7RRAz!p}HGyFc~h>ZBHZMTv#*C`osjMqOi*Kf&)FWUAhADHV8WW+4n zUdIBOZQJLVFJpRLlYd^p7fCII++z2=LwWI3JzQ$z%bh-~!Cz+NmCXo;6}I z7GWhW!aAO{UGKO{~F>_~tvf z0>4HJ6<`Coa3#0dt)yck)nOBDKpXX7GexnLV%SF4@wgvX(|>E&PH!MUKSjIEfeu?a zI&FU#cyK$v*1P7J3aL6$k z_c_XOzvE0i;FycUj`=v^xDXFImf<1C6*%f>!^4gaJR(T|pg>>0onFLTN!T=xQ#asr zY@<4==S1&uw9q-!z^Q$TqlwPtnH}{Gh333%Fpr?On4O3TU+eXuAE%R2+`g)(%G3E-^))Kmc)}xYjHqFFR#hSCQOtJj} zPHV=RtEU!z*T=e>OdEKn+xJs}NsE_tXVFs58nkGsU_Dw`eD=XKEk4%eZRWeBtV!$L z3f8Cfu8*~vre0Z@ephD=7Sfd>HP+u8YNd@(Sc_-UCTf3Ud2VKnEf&3*`g0EJZwu?s z(#FJWdjF_fndN>Jhkzj%sQi=fo2SD&2UNP63)zq18iEer zXN48Sb7>uI&l1QPsQS#&=aJ0D z)3klpr(?UV<7((5C_`*+oCM_Q(!fBm*=dR z=cP_Bb)~3h5DVKp3;HP}oo8)|!t`#8}i-q(!J6}m~V{gsF+(i*w#xEL=7N$#nmn@y;Qg@x;YSksRt=sRC+SaAJrM7iR3Hv%9Kf_9xP4%4b zK{~DCTn4MoLl-+0*O083$GOh?3fFnZF&lqh#XLNL`S==FZeK?mzJYc;$?^RuT!U|7 zAD+fxUO$HCxTbp^Zy|*rV*u~t1-7aIa^pqvVvyDS66f-lX$M|0KThZ_pGE>k*Gf>6()xi~^hmLUdQh;`H(Syv5%JfvzLkxfC z)%s_3zc_5TM@O9u#~%|+Zv5&!@SCjbCY zO9KQH00;;O01j^OP5=M^00000005VZdjTSs;JyI`myV_Z7=LHx&AgY%%VZe>3&2VR?(_eYolltK_}6u&{CnLtF2nQ z+S*#{QoH!Op_2bQca|h%66ya}ox7iV?m6E%_q#7WaOlZrh-kKLoK7~T%3!pkt|QRh z8H={X0v++XV1G2S$%-YcSiCOO+7Yhvab#MR_lsr6f34Q9KLHec3Kp+I;=e9oT7HdBsS{E-Is=beb~E(kWw_ zbTmzcvVRbeGAmO}`A7*Bqk18an5LQJAg9!hHOWnd(6b>Fv6{L%)>*Mtfpsa9M}vWI zb08LycDmC!R~^De1>gfcXj%S)Y21-mikxqgz7-M03DBK#efPaN!M>mJ^XG18Sm?QE}Ht7`dF*&e& znI9)EA2Cg*xlEqH&a(pX_Jo+iMRTYkpH8FsI-Sns9=^_`1+)-|fMBN5a{o|oqyh#C zFfB6aOj-=9V%`#K3)8IfF=HYpQv9?;r?Z)g2gm%8gaucd)Ig2E+NMCb%UZggY5Z_a z(tp$D$XS+}w2YQRo3JC6)>czfP+&P#zLeE^4L< z`B`hyxpW?aDFn0xFe0iB^@Ts(5BV3+MLJywXN?>+DM0J6Z^Y_O93EI{{d8&_9#hJS zVu1A~wNX2;1lT45p-5agq%?;iD%Q&Ui+{z(8;~h-&B*0$KOuG_Vi+S7Ni>U3bm|-h z-)kBhnp7TXCLAj+irE9Lt#bS>lQz+2xV1eHZ&D|Ph1Uw6x0rMZU5YFlM>yaVfDFJ} zemJHpn#D_Nb^094oNM!Cev!7-q$}u3NW;EaX_IpUbeVsSIq7lq+BvSdLv>``qVLFHKA41Z;LA=15m~0{OX)a6%Xk~Ex@PxIcGn2IH4pV3D zm_a64z4C}w;M6jCZmu)rxGr);o-Ci0vDdRwe?$4UK_L zWj%v#MSg0x0QY?>eFrMGMuT0lpNoQYzex{B6gjPpU4d}i zpa-#|XdpX&*QAFtxAH0#Qck6p0993>Er!z9)tFigrxdP5-9f8UVP2=lnSUlmV{OV3 z@j!Ro%K2-u!#ee5?wL@u&cAfXz!UJH40Nl7oq9wROR%n3`R~#_}PKJnicgMN&J5swA&+Yx|goiQuk6^1B% zUFvj9Q*(+OJinwIfD3jS`)q(%Oi^7uo`KHsM;wkr^O24moFc-dxh&i)Abv^s*o#v8 ztx3P5-@_}xa5RE7Q$K}OsO5i@bT5m#g`r`~h_yMjWqwy@Yan5*Nq_av$e}X8-Lql)_PHBz@&U~>C8EFNhj&livto|Oo$TJ*r~H)6nOh1t#ART z8tZJpLLt>=I%aH_`74@rhWv?{j3J&H`qXi89=(JKTwt=B!S9_>HOe$6x8y8l8%7f; zIN>}lHn~J-zkn-lxp*9p*SR#4yc7mjwa22HH1+zLI21zJ z$%;WJKP^tBT)WD|dC`nL@oX9GV$9kcu`+4&6F`+5&1umXJ6Xgfq zo59s4*Kn=uWq&0atwehi*3&@Zn}J21X_83=F0SVjbjCwz%9xK|lTTzkl}vO9YfLLX zc~r$k&6AlX3^k@dG6Z0=Ma6oaWAa=+6?oVj1A9kIm7lx7?-#7h=F?0*o##sy1b@I3 z3|07L(L$5Y5D>W(h|&k2&Bd5wr4*YunqJr;m|>$1^?!vR`^jOXmYD4b#9{#mw4J*E zkP2H7of}Yzb>W($QU9lkY&dUbn8@yWH%CX?0 zNHE%}o+^8}1(}}LGA%ixRD6W#n$MG$A`&SGguxfUM6+t^4ZcWjgpP0|ZZMu!pXrK4 ze9=HW6o2;#TZ{V=L7%!Wd>O9gQ>)G}?#3WrV)CW@S$Lx(kbnh7%TjpqHS_08zFcUnk$JW1 zdKnq!kFi+T?5aT3{+1h$rOs>6Q-zl2iOL{6&+$#9x+#)3q*>=*E}( z{eQVQHh%>RmD0SGziRRgd?OO9d`k($;D{7?D9XIT)7s3D$v2t&b-o#FE(lvBtW{P= zXKMC>{{qn&fjQ*gV)CuR=L_Wpu`V7BgIp{NB--Wvnj_9RP3LcBa@!EeS$Z4fJ56ff zyFUGSpZN^Kx6?}o-=i!NUXLb!+vNKg&wmhSptIA8v>w4{x}iFVM84nT2jm<^BAVth zEYFr&VJiGx$%LR+;V8Nv5i%1CHUwnOqi7`B5~;>6xl{wO0Pf4qc7q>R6T8J!2II}) zTpx`AXD)t%`*nVjsp``{<0-tRzT=s^l$RO&GNCMZcCXbN8H%xw$-$IZI6|-WStd*UCpe5nI7LNDAGmGNiWN&&WWTwOHXA1d{hqWa7K_HFrhlOXJ52ST z(bzCmteem8@drA;KRg_V3NraaJ}7s06v@b6{we#7>4c$V)8LO`l36Z3WFwsihB4Ue zQ`)q#1fabB8^K-!ZSoCV!l?sk*_rxlhJ2ff$Xjr%D?z?#AryC0_litxLxYrT*X zN%nT+4@@5XI5u1(teNxwV1FVw%Sy#BQVF+k@T0u?$N-f2BM~d6{#GH{I5U4_a47TA zF?a;oiO5Nl!43T2hK@W!dI`8=9Pr_mHT9S5FIvr#{T7E%L#SJ5I`O=nV&7E_qcqHEB;ftJuM z7`=-c=^kpLohYB8Wy*l(L1G>~MxUpv#9#DXx*9bQ9J&u{uYZNPC&9R1pf3U+)$rDr zU}O!lHqw{je>?UI(RK6{^aW|U8g*cwqv(3{Iq0j9IS+0~?QlbOha0myd<{FKq}`O2 z_H{^0$+#IZzJXQswBsrpmsO>a z@GIVtb;(f(5u%kOe;6sIbD&K#Otl(-UInMDRpzZsZA&v%Fqv@GBg(vP09=e}!>Wn2 z1EY3jEWvYX)(xo1(eugv2^HgvJk5qP;r77PT^NuhkAG2;`qDA@q=Nl%=oPh=ry&ct z_oIBevAQxz&olvf-}h-=t>lNWfX^XF6BIgoXhN0OnZ{s!$pIKbq1Ks3;;U7I=dM(V z;6f;H5w;G{cv^>31Tof%fVOBBtq15VT1xH824|%dqMxXB2Kniy>C`cwenvls>f>n+ z{eqr@>VIW)BK;C`WT$HS6>Q~z%;^eZPGzB~n3a-I3>m3eH)Ll$k6B{1qscY^MRc8l z3K;!5?bWGXK?NZ3Kow9IY(3nPf|ZBSlEiUR+Hwt5V!6g@SguKBl5IY%pXysm>U!zd zVnVN$q~E9*PSPI+Q`I!(@-Tio;O+?ajM8N4gnvmkQVqp$(6}AJH6&7ePIc5f^s$|N`Rp_MIj^O%m-BrEURRB`Ajzh;U>}_# zwW1#2HD8*BYI^f``%G{CesXzDUw#i2d-J{KeQ2RbvIoPD!X`Tr75ia<`{C;sQQDPf z)&g)un_;bP*k=oDbqSKorLgH|5tf%B%YS|jnd5R~$gOlPT>)5MNf#r3Z3G;MtABGKjI}La@{XGr%aqwLV@KfkF z%C`>e|0LatniE<)!d}#L=y5weh?;@jJ9!eyJnX-gCvzEO@5J@$<|(+D1^f#nTYv5+ zw*r4~2>6Hpi#G$0Q~%}>qdZ9Y3s4jkd2EmkU5KK%5flJ;RY1K5$NvgybM_mH$>VBTl zQkmq6UaoAcE;5o_wUg_rlRS%?7=Mw=vsqvop!_ zT29jM7&Bqir8P>K)hTWk-r^#mx7b^W^SNL53f+1 zD7cH>LSt2`(TM6fnf^m)G=HZWP5rdDg)xosl3gQ-3dq_;HOR0YKG(zNBYUX&g&q!M z>Ol`%nR=UtL#cW{UDo2^4K=;o0b141^(~TNkb{t8dO5m_>)7XD~Ww`SZwWF1y!N zRGZ|hlKce^Uni5)u2&}c`ab@eN_Olk5N*8qN&bf9Ja-C^g*{Z@b$bi#rA0~JhJ0F> z_Tftss%lf z!`(7x2b4=sZyt1irfVmW(x zq?41!UOG;aQ;FB3lGC`s2SlBS`W-lE;UVsDW^qS#j?kSOvN3nWko zB#L%>jr8u%3Oq;G=1Mt3lK1!VkNfx+Ip^LzIJd}Kl%3l{WrLd_M*@A>rj*%f4|(`S z55LsMzxCy@&wm9npduqL9T{fI8U+#=K(xKoor;VC6&Y@c3{xV*Oh<-Gnucn6UAw^= zTw;cNpG(YuikQJA3V1M_4)H%ohy#~-O1+nNNsy--&q|rr6|{!`+~PC#fApGHmp##b zwMS8DUG|iw*Rk8@^g3iur@ZfqalDRH#6DflUM=S~ zgj1 zJ9v)ryBcBeBN?ocd+QTb+d*!g$;%lmnWu3Fci`rlYj3r0L^;{M)xH(|*J*C8m=9@Z zYa6s~Tz|rIwQIF&<*lu~ti6SDyY`Xx5w3s*jwOyYC{K2@Ioi;Fvg3NkjkvE<2bUO zdgNWR2kvudy-Lr}JG2{@+8%b%_kVZ-$};*s&!oM$&z@x;?c+00E~g*z zb##DlrXTaY^b`I8{ghv&pYaFub6Y+(eG{7(I0Hv>5sMp^s?<$`jg#Bf3_FUEA~?Qi+wKr)xMDaWt9j^sZ3=y;mD543iayp(qEEE5 z>5#UZNy7!HU4U`}+qLhorai_EZ8z)MkJ!+jr+)2aHXW0=z%i8z9n;z4sOE8wdMCu}3>v(G`QgZ5fQTwGjcH*E&` z^5F-cBCc+DW0EZo`xe3{E=5<0xQXoP+Re5tiguf}3H&}k3MmD)O?<1(1+Cn$^nW%* zgbHEj`HBdYz}hJyv*>*vQBY#EZ_h!gg48)9RfHujufl+7Zyj5|yty!j@;dpri zpq_>Q)zT4Kc^}c*0gz<#NJPr&L-`+2 z!2rngr`aW^F$<1x(Fik6JV@gPXaX4dC;~>T7{D`OBrwK!&JD;{yP*rzaXP1y%~mMP z-Bx5PR*y3qzei=Z5~uvt;60n?bnpKHP)i30H~Rp6_7?yEQZN7jP)h>@6aWYa2mlIh zlS%|Fm*BnuDStyTBpE`{2$K+$Cc#hy1QGqokPKlY&6x?svbu`v+E>>G_8RM|s6!A} zL0w$ST6b-$u4UDAUF*-appf79-1p|q1OoE^{8@bOzFW>c_tfvX!#=y~k;jPWFm<$# z6jOOaTT5k2cujk(ttl36iB~qXwXTZ95|LQEGTPYETz@$~(H2`f_Soj=^5s5qGv%KY zUKOrv4!1T{&RKR+q#?nS6>V+rNKB75N09b2`FJ(vb+qnj(pGM|M2cFs-$rtr6=%KOSjniL`(p%x;pTBHCOzBOG6mkbSC0W`Fc9gg)Bj_*wI#O|9WXM+|&4^t!HQ zPetw7Q`-ymUKZ9?`)Fv&G%}|8se}d^Gy<})RPLuzD)Z4urovuWGO3&@a9WrG(}>dg z-7PJ}OlFslM)j~sizCt2Xp=nTHE1l8XN?Hbpgie6)FhwuAI9W^nTgR!_XLv+>4xxp ziGQXy7kHby!%g1C0xIG*TLwD^f44SGbtr7b&XgWR`!ijKmTa!UEaGJG| zxSxVF)1;%Q9+HTh+!1bu5ft@yz~g*0n`!8N*tnl&(OfaBqrrRW{wQdESiYSh=dy> zF&~AQhU|XGHDdQi96Pxq+T1ATTS^1v(P&bHmP3ilWPoXGZ`<$TP(o~KTSJEktJ+U1 z=p-LSnFj4^o=GdI8PN##H?OTNF|9cwy-Wv|_DdDc9o^J;>Ir?go*3eo=5C@x!2v<;=>DBeoxVX`& zO#OG4tF4M`I-Sn&(XSwyp3B5x&wmtysO!};&qB!T?bLJY{Pb&DXVN+J8^qg0o3$H+ zS2}V}-HP-4)}-_3e0bKXaC1jw&hlRl56-@L7|d z6Ak(yf+=kX(ya*}{betPaO|x5*}xUq^gO+2(o6I*&M(%0z6ERBEq|MuZ_ul7TMY)p z#s6l~YZ7EkP-=+8<9fiLH=u-?O5tFgkN&=wAh!S<3voZaMemsOZp!?mUzk`YA^v@n zK9FGTX=rYXL)HHjI8?c;t#PdtV?H+N6NxcaFt=o5(5HIVa-@t}zd@hrFP(8~4f-6y z87(;AOOw8m{O1ZcHh=o*8}X6<1(ZrldP;LRj!gMq`rb$1^~~vOV?Cs>D5{E6YccT&#>^lOc_=mSrutEX%}@v2Gi!vn==p5e{Te>+0_O! z{Bc?=))p(a4I6wEYu#kCi#-> zDJph@xZ1?5ihrUoVue*39fhq8as+?RPuC7b4rc2)kB@^ZEVDv9qE)Y%It@$&k2kh8 z0Kn`UbRg2kfFfXR`%L|y)-E+c{_ZImd;zey!-+&(suLilzP*KfObd&V?lWi$sBN$! zlF-6~TldD89hii0#L-Pp!x7yy+ddfBtG~f3dX_~9YkziWRQ>-^ksvedUroYPwwp|A zdOh&mIz4&Jeoan`RMs5Yi$?ZmUFCl5mC}T)7`NX$WvisZHyAao*Mg=7 z9}dsAVJZ}3Fp3&fHye1W$!T~1orH2jjVQPdRifbxY)fuBcPtq;LP;sW>1}`Ea{hIx_`P$xp%Ep z2mC)KpU6uQ$bes?jp0Nj4e(H6?z=YFcj2Igx_`{%2F7vx(19a>y_6iOqsYyMuc0Fr z)05pIErbSXXWPAUI7m(4qSf>}0U(~m&5W?$!gP?9Mk~?<1dVW?fCEZ>5^J1deaB61 zV<^#s^5w+w&V@|o3VWvDHbzE_bHc)rlh z%YO;IaSNj`%A3NuPWVr(ZH~+nvk>K+Zt}1A3=7vS(75iGs%H*fn@zq&>MU<_ zq_rurLb`4<`F7bY8+b8MXYDBccL?P>_ozl4V$64&e2>%_zU9$13&Ks1`z-#0$$$6B z*?smEi%@#Ncqtd$i8Z$V{l6D}SNNiQ4q}E?!-IUcD{5TL-O{M+muc=Iv zsrAD@+T~tGXHyk{!aM*sf)omSQGXIrIYvCoqcL$~Z-b48+#*4_1htNiDZIkRPx|=z z9`|!c#A4yK5&``D91WC$^h<+Z!W8kra05v7^ru%$ewF`fd9WT~s`(`o-S_p%2AU~^ zUpM&;?E!F7+1N=4%x_5q{JXv!+Gi~7<>CtI#SVBjv+nrHC_FJs0$-{tfq%Ba@54?q zve1Vn|C2wm4m!n7DEoamm3g{%gxbGM{#0r!SYpCTt{Z5P4E)FB&-n|vMbAjcy?bbH z=MxfNK|B&&_8�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

Y`3CVp&F_0J!z?yO9KQH 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

FeL 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 z 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%0Au)S5sveI?WVhvy^ z7FI%_!9W!ZKl}l#z@<7?3e?Y`lYvGw@wojKO~0}+e!BaVw3$uPvFa2@^HlPqnZ7VA z?dON?$65j1u@llyq(18m1W@V6NOaI$)+E&FIY1$zt*+o+Y9 z%UKylMe|&&XON12o3~04Z#A$T-Hb>#>rD!r*LdcVR%z9n4b-Ae_HHrYm-g*dW*?Rj zuH#mLi_akl1K5SP$)mkp;F>w~Jr_#T4yncM2C7k`V^7JzMX>40Ok+&|CN}F}l!Qtg zvu3wjW>PAYM88yMKp@f>pM!5s4RqWgu&Bh{k?-rXY%5uR5}86;iX7zi&WQ9T)!MAU z;`snFfDAUv1KI}4QQ-%p<cXF$XOt<|bl_JC@8Q z^786pGu*L%VJEe|VZe2Bv1oK?XlTvQWoxo_e{|j2wJp(|TjSk{0c+4~PGua|%p@$X za#Arz5hZlGYV&j2Fsw+;iL7lkbJ*Q8ae`UXGlRzc^bmRUjP$GdrdD-Gc!-eBR_)QYT&BRN`z=!Z* z9x0Kw%uFdNSDsU7xBBrQQ&B@yYV?pmK&lfhYWOh6ZPA$ne4NqSwkMk$X%9$mJ8TfF zDx~Xwe!{?`3KZy#Ntyj51b)iErzO$HjRqy*aRZ;hXNf*(E7R`|sQlR_C!R3yd5lu{ zelwl7w)LGI>!+3fB2%*R;U^6|C4IQs^WkE-AZPy2z%w!}8{UiuD>7t`7#Nci<%x9G zvHbWl56~9%&gVx#jpT*M(NQ&0zSS<{|40CTllYpBKPn|IrB=^?ogMNL*Yo%~l`)ga z?tCt1TaIH9ZWqr!)-*HY+mCPZAZulQ{I-EVmYJiBYXdlj7jztFO);~sGi^G&@;{Zf z)tLouWH1Eq5?|(qitvHaM7VM>G^ur$ATZS@wI=G;!)({Fryq62MRJ4?2E&$^~y6PFOh!CI)^c z?H{03xAJlc2Ri<-#43})wrN%!Kj+PVOSB^tMjdlF+I`*5S+l1@PCPFb>aG8ea+y$eZUr;V*zKB$y<5#( zaofQ337kq%A^f_~1(vI&8QLK+AU(vT1T&K~?PRAnb_`J|0<Ik5;T$;TI7)COZN7cJ(Yk)7H;%TzYOw7pp10TM73#y0dhnV^u$6-X)?ew51g|U7D}$Y}K&bo}7ENMfZ}9pFbcM=)3y2qR6Y2VK1gJ>Q zC>Dn(fol&V;8O%$(u1Aj*fEBlN3bjs+*uS^HHlrlp{g;ogeoU++cJ8OQU~jB;5}DrS3FOAHFX$XaJ~+&e((6vCR|WM7zQa?l zH1P+VSS{%X_~Dj+{DuN@e9tsr@Syy>r?lceDav;g@0VN;%y4}~em*KcA5*NE-jGk} za%iFdFqhO8@CZ*kiYvyjqkvBq@Yo|*Dn)X96rZEm;1^2bzBG!8qj|KDMSdpu3Dq3KHKCdUzB-DQm?4LLH4GVFk5z{ZdPJx?W{hG% z$Ou(G!4aDRp5ycw_EFxd;5Q2R)-9u06RQe}0{-MEUZfmj41XE?Zm8<3_`VOTf+r-; zs!&w{f3JFf&(i;2(++6&GbVnfJ*qv*c1(LgdkL==+kYyy|IDajjEQz^BhM8K+bbDg zSCe)P!iZrNT5%QH(1G>XigxV823WWjIa~*q)Dd*zgV=-zaXlW!4LF1@7TlXzTW`S^ z5y#WGi3RU=97Q+2jUJY3J8%NG;5AA*Nyyf5n=tTyHn9M=iwGad*CQd~Y_}sRZsC*o zHuQ;M^ow`$;d>8K;=R}_9^#Aj5HrGKe4u_7Iq?kkiQ}-vi*Urt$ctA<{~?CN2@H!@ z`K0_g?i6p}9a<&crPbhowgC5N5xiS#!9ndx+^fZLw{{!u({|(iS|3KVy|`c7hYxG} z@ql)J7ar6O;3L{39@4&vk7_UAVeMsnO#2=_uDyyw+Ancf`!ydT5|;`DGGY$?1;6CT zul)-Dihm0{(-&a%|O}r1dKapgXn4@Sp7I zu(XG0{W{)DPP>oxuH?U~9mM5q1GMM@{1v-;&u3@m|SZA4`M*tR?k{{aa{L%rIxwnC(v@wBr!HRTDm*)yMmKRVAY~-jcLM4FfSUCdvz#mV%c;es29um zElJ?;;%T(67b}YSpk7?+iRks+K3){mG!5eig;)}K{_&~1RHl>1E>p+eECYvdQGqYf z)t}_Co?>!;nh(=wa50{xvmYT8jS+&z2_s+TGx{q8pK;vDkUM}00?spB^&B~0CzaX4 zlv{@~u}ZAwXfX~KdtrP)G)to@w+^DB{v-mrSaT9S5&9jH8jTy3mm$`Q7R90QLHLqb zS0Ue{KMP!O-L#@stp#meij7ul z5$gm@U0M~lx?6W^wXNOM+S*#BVE*@Ep5R_A}%Pi=aY$0xb~z# ze0?IYE*vCD!HQ5zB#=nP$YWtXn%Vss*B>Z$M_agdK{%+knl*<^-O*&CJxK|N+lzlg z(OM&`CUe!EfzM=Ti;OsL3foe4Rkt7!3Pj4K-R z(&n>LzmYk4IObV6XO;&eG7irn^Mu7f9N}_od=Ho;nG}sY}t3V<` z*Qkkw7QUt@+FB_=Tn*J(I2otVP>JZN%j}zFKc^JuicbUIS117lA*9(JPw;JoQv}Wo{fQUGPtDq zt20D4DmG%OiLVR9tKNSvvT!jjVITzB+k+AMq2{onD=msn~QsH@AXp5S{H zu2qSua8oRl2-*{h&Gm}S_ti%ERw|tSz=8*+2ipWA4pAl5CO3as_#u8oAlsYRNEVd$ zuU@(3ReAQ7jAs+UjzsOEU_2gZ2`-Gr+5!m^w^6e+v-0yMT~Hp0Cd!+m$w<>gFK)(E z4{jGsKSX^zHKH>em#4Wb=x7YKCqk;&+>A~OcPP}joAq{HY{ygu#k(xrtvyIkH>qN+ zpns2rpQ})HHHLqqagN-J`#jj0$wS(7E7!-Oo9tTk3*67dA82Y?k!){|QK=xYG%VMp zSz=u+8gP$^T}0s|RTv+z(2ZXb9b|9-5;9O`+I|R+cNE)xqc;bodB z9Aya9ReZ%>hwN%{-C^Rl^u%N@Uc(z6yxzyhSpZphQ(4(di>5*A!P^;9l^2&}Nj>-- zoga<0Xz+i>10A(1PB<-R_VXpmWj4zERKs$gAiY+V#+PjBnZYQSb}D;_aLIGBh9BJr9e zPZvz!;;_W2vV=RhAsGnAO>y&`7A7LOModcz#LFzk@A>Olp9GWhhWw#yZ;C~x@n|yE z7&L#SnA}(xW6_PlCQ}B|F_Ea+s>G5~8O(NvTV;EvRjL#Vj?J^@VKuC#3}I+D1mjBA zVV3x0IJ;0=*SeNSG!~p6hzAc+DAy>lMp`l|lL=Q6Hj2>^mXxb8Ckfk386yDemyES! zoT^_%x_-@%w&_kx%^Yn>MQ>$eI3_eFkxG9{s?@{)J+VBf;2&uW>P0n{Ocb6Wyn1e7 zIMBjKJ@U}SaE+=1<7BcWQ_?k_u2gfMW)>(40oMInEG`OgHZ?~VvvpNLoS&L^C$|70f5uO{0 z`_AylAWQr8Bk6*tHeF7bVXj)g(vnj}ovq`cbA$RAQ)dc0|6;At+V(ILW?0F;#x@8A zKb^gqurI$@*q4vY*YjkWI8)Y=<(YqKd4NglOrUXqsJxb5mhGo&swrk*kC&&T9>}fYL59gE1#cB4i)`;YRZ54sxz_? z%4XWzQPUo+X;!#bJ6~kU#p-zJSs#cm<|d{veOyLN9aUYf4OwmqW75G-T1cx$V-C)z zh%&t$PA*MHA+ktHnC;s&2>>wz^(bQzQE3$dpd%OS|34hhOb@mnCseZY(FQk?1Chz>d|@iljB?Y*r=iP5h^`A60*E)@_8RF+0HA zLbtLS4bm)V(Aef|T6&05g(7yBt+joGp`z&m$wai#&bBIH{o0a8FtVl8i-_aBxn`R~!>?aZ}+CbcJ4Mq7gsCc&6j z-jd((3@z^pjy}AayULXJsFK&K*8ahgKdQo5pt6AGszyJsuss}T44x1UtL8#4@yLHa1*7xDCwt}0Nnbue z34i3@mi$LPqXX6@bzd{(u=|=5qm4-&?Oyp@_Il(C)`COPS@J*G#{g%UTON%j(z$}? znu7-OV4H=LC6J7pSIOZW_fps52dhUBBYG!H<)-BjUkpX)EGwp>Gp`viqq$*t8(uOo$u^3N>S!!Tc3@Xo9ZGAMt#Cn z?A@7jtV^Hna3rWL04Mu77u-;Nod+gI3;1sE%d2kP^mu=fp0_w!i~*z=$ginG5v?hWIGLm^&(A#G7RCf07Eg16!sQA#mIRPx8+kl@5bJ>MHC>qEclw+uO!vEdt|u@^jh4*Zg;{Eb zLn)lF4F!MdxnQ-T<5#JAp0o?~jt8-97gnt9Mni+o*@e^Z!&$pMpeG#QH8o{AM>cH2BOe#Qg=G*wc-T4L=dq33mkuGLN#6F%Wz0f@5BwhBCYaPf3dH4 z4+?$7yKrMC#&+Z8hLT&m@MHggZftMx4RAilLx96qv?7I{bm7j2v-_778n?hyvR3J>nWua#?dVo%9$YIfr}C#p(bQ501*DZHv* zb`V^ha;aQROkB$0zz@{Bx(x6;pd($DSb=etCg!rF5LA#f}~>qiM@VKio=62}vH zb1?@eU;*agWX#8Dd^;Np(TtN2K^?x1Q*eJd>Tx9&;bttxZG3iO33g%`?#FUGgOzxd ztKLNe-p3kzh|}>;oPm8fQwnjGjKEr+an6<+1Y`=<$#gWzTr|llw8&blmmorN4jQB# zVTq$%Io?&PGJT}Q2=#USvSw6zK@)^$8@p3l?saU{!)Cw?}!QX$! z9~eU=r2Hc-Wnh}zfIsogfjM#|KER)8jRo>e{Dp5W>hT}^6@TOEop=#{r|bpP=}~;d z6<)cBtMnUny9OWQ9}MviX^&4hW>L4b)YFY3YBvx6!l$f`a~Ze)CeHzs_-K5F|H47J zN8xjP!F~5q#vv-=wZ#9+O1zH}@8^Hlc9itckZupkJs9r6IQ=(?e?ZaD9?U>t|0xgd z)qjm1Y~Ucsty;ds@(j(^^3PGa7v^3#dzeoO4#5G!=Ux=f@_%A@m?%g$4!GB=&5TsD8-c^u?qn(K+ZTxD*lEYvT@^pRYfUQ`v<~Z22uc zx*z%3+1bn7Qs&MmeC@p&P%2fuis}5lh?VpbmcmOJK$qbNd;??fP0Yl%n3TTFgmgKU z;tD38?=oht#96os=iq9D8LCly4;yeDS6$Db`aZ+_2K)?LaWA%^3pe6%+=RCY;=N30 z`*DjnaGSWXUA(wmit&FFDPf8ojyq%|?vx|(Q}N?2na%e)+#`$eb7{a1S%-U>rgk!^ z+$US{3;8kbXSzzs)98|SuuI;@Zlf3v7=zGlR3l~hX-Ac^wy-N;nCOH7YWFdB{*+-f z3Ll8YH-nn~QPxQ#18NN3lP117DfN6NN;h0Q^_5Grw6OUbDWiY&tRbUOCF_}<9F*)m z36YC~^1ZJ4IVs((Ot7DF?GsF^iq1_5m*{;xIw`U3Nb_w?r=kD`_Rz#G<`4T%*qDrw z>8^d4$W*Ww%*@@*fyM&2!M2{4YSJ%vjo6AhpR-ghQH{tgD7R62sU0J@Fw?APNZ(;kek1SX z6**k0)VrO|5{Fu|TIU<5BeRrUjh%aZ&diXhTAOqpa`u0+9d&TFz2OwW2>w1whdsuG z{y1T{hn3(7!s|&)z*Crlrx|e1um|`JPQ|mV-A}Xn?_n6dh&W!tg?O1Bdxb&sTcq$B zcH?z&c>^!tP1er0StH+JsQgZj#=CL?>#u_O8MYVGFz1_wDqDE*%eM$$gEexhte0;S z=7WiZ@6dnmPD*s5T&|!>xsH=7Nav<>)8xCP^H9DK47X3IhXQRS>1}SUS#Dd%&0g7> zUAcuT6{N?){1QV6O^?J0;V&`DMk+tYe~yCbBd7ikD~QUfJk{+w@+~AfrYakp|;?3es-%Wj8tWc*-uomcIz~Khn=^xUASU) zRZ4!UkS0GHxO6*)^;x>ZCapgle)V}wu%_1pJGH2F{X@7qH-!6dq~bTTK_-Y_=(E)? z^j3fK`*R+DAm{3IjP>$dl;!!V@}_$GvZ_iOg|X(#5ho|ks)+|ZKRkzNZVl`W#GR<- z?+5()GeP_pLhG+Q;Qfu^`FFO9AF?U`h)MTjHrD^ZDz=$tvcYU-2&s&oWMlaqww713 zsoc(n@-B|w&nEIMX4g-!UtH*sN)eeOhAe*(hpZB(tQ8k4iJKPiFeF@Zy?EtLDU=;z z$!;l)yDy*G2T7I1l=4E(8c9G6atzMDa`Seh?XLW#^)bKGK;04rihU|al z3NN)$d=-wj69EqkGwehl5scDwrtHAX^z3d-O3yxtq4um!6U+IurEASfw}sN# z`?qHI--3elUai@^m^#usv}Sk6q?3zL+(XCN6;e0TI+XxDJRf-v80|;25995MxDO2+ z?qPOv_g@3 zCl>lVWsI%IAHg_(0guhSz5P*k)!L(?PmLc~580hRsoFzR%1qT*K7DIXN^O6xL-YLV z^vkr~z*?j3OIU+U8O2I4n&-bGFjC4<%l7Sf8N(CbQCKWvd9obGQ|WkYV68fj*1K3H z;7X~)jm-M&w$aT~>aRHd9BE%CpZCPRmua>wnhpRHZ)&g$tLe)c>;^{Smm2I2o}RDO zkaY5_e7T0Ci(uQJA?e0>Siyf2?H>rDuVbnFhJkLfd7UQD>b^$-b{gsJ9a^(HY~c{orW977i$OBc_ii~V%*EV_6$rpO%3lH)Ot2k-@KL6))ci159GbQjA4?ZT66 zjj}F0S6)lIFrIbc89cTt7tUe}@j9L4wfrk}rGjV23NNas~}?76vl$ zm&w^Ug2$($q$z)^xQUU580xNeAO;%_LxoBi1{f}VVDqX=cv~mt83zpvbW}2Z_QuV+ zoVdxkASv648)`}ohQhL%lws0|GxfA#>FJ7^-n$K>=pZXRHB=`PFgNxF%oKGUO^CaA zfaF1|)6YJ98G_-#yKR(le;zHaQpaaE1I2|ZH zoN5fzfs(n=vZ-3LR4Nqq>*K9+KL%uAPZVsOyjPc}Yw7$0vJPga;ZVF2XVG)jDPvH| zC_CV?TsEReHs!#gI1LNK@af;-hC1w&koI}ZHb%PDe=U377&X`Te^5&Y2nNyo?#37Z z08uai08k4{0|XQR2nYxO7;cxrqX#0FFsT9zm(I)w5P#iTd3;n=vi|D!z1_E)o1Lu% zv1t}VAOuhmn!q=mml}ZpgUeKI4E3h+^Cag9a28H^6O1$7S4c znNerPb#!!OQC^*MZzmxnn0fF0-XD*j+_P2HsZ&*_>N_nj{QT$>08Uq&4k(0y6_MJ! zT7P|AG=EYZ_1DJoDk9;PfoMDsjpfxv@@7Ov%nJppsvJ-W>C5~p{dpmOxH@n8(q(~) zIKduC#Oo5=IiXlC1|xZsf}y~4Ia?Z!2K=?733_aKAQXs4!psnhB%&1oLS}J`G$ss& ztC`Rq_eZM(EY87d)ds?5FB}ilbI%zKR0X1euz%pzszUzim}nE{oU@uh<$|&*60P+! zWvaPJG0RT)s|9&1P+e5XWJRn@Wu(?042z-Dk}digOBbyx99T6zxn)XSC=vCCW=XAL z%}OWY!BE~DHb5lGN;^w~)nR`;5#@~r9z&3S>^TdW$ze*4HSe)FX&61fA`}b<yFFAYhvMk@TFIsRx+jIBv~ zz7*(!4LJs~;bEcinjmkUUEDeUv&Ob)g8erryVVJ&_+vG3vBl^??HQ(`UD|dG(w>Qq zmDsK&9T=Vr%vPi>3`33%-G$R|x&tE#S%2+lVqg@`;DgDC8<<_(HoTKMw4QQc^idbV zR76JsM^L@6ln#fEGugqhXoUzs;jVE8GU3!Qfv4luaWSo95}^yzObk~apQU6W$=stu%?y`X`fW^cKHG^C4Ot$_@rI}k}4&B{MT;pX(?LKxE= zh!WgMidL&L1Aac63o8&8_Dc|QvVV))p=&2Z2hDL{6`^~QdR(GPq^HWH$dycalrEpXt3+JkZ^MTnEKj~hc9O=7zr16hYua*^#rWwDe%so zH7R$LcTAzn#cXR0Tr7g2%c^KF9!SRMuL*DD%OGtnWrWwma>KC#Zz3A@MSuLUV9Xcx z*9Ky~c!f_=`zj+936YI`E!4jHSg^ob6OY&V^72-#S~YalDMKUC>bw!dh7He~S5jPB zQ4^^3=LW;ExSumNbH#$bn4AK%72x7UGuBhzMe=lSRG`h01Z9da$fi535_;$A%7F<l}oTiv@YM}%jI|)iZpJ08|z%D$-+H)L5 zuxP5_#{dBw}hC_AyQM5yJqzSQqGVm&1V*>`mm4SL;z;?AQ((wl8(TZFXv*>t} zy&jI_woJS&q@JhaU6xfFtFDOG$A$d&h5Y?&TvoM6OnhkIBY!c`CFGCAi_A^M{9nZR z{>lj|yQsZGi3IhjfzM<*=bTe1poEUU^U?Ske9q;JHw{+m_>y4A?qUE1%O#PeI7v_!KA=}*+BKA7P^vhf zRuPKC0y?D&0GDUpES)mtNTdQW>MBPvoiP?$K`AckL4R%s^-RJOso1QVXk?XyCv*b! z;=947EKSta@&2&@SM8py!>O5A*7z``=+uX;G>m;g9`*(Gl>yMs9hl>yel);A{TW5J ztd8d(iWxLeM1_MbFpW>_qHM}>(4eDn&+1q_P;1a&8p2nIDT^c3)q!XyK!s%NZ<3F8 z(oh=apnp8Vi5-yiFAl5>gbW%^BiPClXU?2HlWakL+C<`;D zdxS5#&`28Ppwo{=TCK$xbO!m@$)N}*g`(_V*zLH0wzY?J%4g>ccTpjo>7X%5w+pB- zePVq@piWdZgT@N$yV#~B>~i^jvGa}Ka>2QzU4Qh=1RNcSka#2o1!xg?g8MSm%)XI) zf|}mQ;V$>Xg=)G<`GguR;;-@j4O`%qaQ+isjbJ*=>Nf^gl}VM++VT0#9t?+>Ehj~ zhkvcNoQoFFA_pyG2+_h@#7{{XQHw=Y&}C}lIgz6n`JO0VarR{Sv4`bTj-B6p+?bQ9 zsK!CO+E)q$F<;yuEo0v>7F;~4`Ay!M!=mKu?<2lcwZ#6` z+2G?=#{(Q*eX=`d88m;KNO*MytsquWlYbeV+wA${L}atia#|8`J9*SHl2;~aar~7A zvd~qhdQKV^da~Z*xuy^^-PY&UMRK`nL|5mU_@$pt465B;PhBEf$4R%q8w{I8lkFwY z8ZopU;Yt2U3iTv~ClzC{{zN=t{nFqq)ag8dBJ(UdA?N~55tfv^qjZxnn=UeFt$!$3 zsnV-6lRr=Bw2oiazjP$4rt@hoAq@UybuD_K=VgR5ENVuQbxD<4WQbXhTxxY#`ICuY zze3g_u`JHGp5JiEGq#o{O32j)T|?IjFcX_tTNhs~kz2V~xt<@-bi*--$*L|oF(g*P z5uJX=EUN=Coi_57w6>85J~t5##(!lj=){P3fM@=&h4O9Ti`Drn1oGw>3Pe}((}CZs z(a6exB)zp6YD@er0lefA+Y16lz6u8cduNM5w@WZBy<mWR zmrWDo8)TV9B;L0SdYj(ibT3lu*tm)1$BUKs7~IqQBB?TFFVg7)PU87>HJ*y3MJsY8 z5-^jfK(`+Wbo;SDx8sk8ZuuRMQW=aS>gQDjCRR_b;0yUFZ^Jjfrhkxa8LX5Qo!<_u zZ3X`w@1QS|znI8MX$qVbSsn;;m_=RmIi~6KwVCvfo9CK;wdG95uZ8Zd@^;AjB|uI0WP{Yb|fhJ0CRYkjQf>HU3yhBpQ&L zvyFb@{NGC7dJc!>;eW^~v&tOlRN8Ll5Wk=X_&Ml|Cym&Qp4zjpz*v%!6H%!=po!d4 zl0V4~6|9V#io)SQRN{6~!R<*Lu1Rc)zWije0bFTa;+G`uxx`anwmO`$by;l}t2L}P z#cETnHVx^r&9K@`Yo4pPY`a-)cdPATwLL95H%?$}dU0PtZ+{-2Xs!2=>rU3^BzTyj zFOP^t z49RAgL>P=AQiEI!WeQWnJR!(+gjX{G`?b3;tZWBH6z61~(u7l=gs&tgGvDk@%gHP> zdpZvI{H9 z++E!E9f2JH+_vX1dI4#pn7Y$8Urvbti5VRJXKHZ4#f`#OHQNx~9ynN?o=# zsFMY!JAEe_m^t%a%QWfk^d{Wjgl!wqMJztlp!RX6XYOdiBO5TfQJAC&k8ePlJDsih z6x*l){eOidUa~~(5&FtiQzp}}gnd$irx8n-x_q%odoLo@0Z;BLiVD}Y8+36^0q3JcUFb+c2qd&7Y;nH?QZiryW70x-GtlC4R<%t z1HAiJ;-pl{y>H~1?{2_vrL2WgR>%7IGMd#HD+4^EbJ{fXxzqQ7^MmN@#ZjtlZ(eG| z0)KZ#6TY2m>9_Gn)lPihfPb-6`|)hGs9Cw$rmYrr)>ey-RZnR4V@rr0JVxP7?0M_D zmP`F+vA#b^jL*P1K#2-CYE0mnP~hSe;_1%c0x3+vjpS1*^;F6s%6SOR#by zSe?5E+p_4yCOWAB!+g$6M(^I8bTad}8Ywr6PH{UQq0>|h&Z5!6uY=vrCMuA8pSw|SJq-0F2!`L#|&JDvvDI+-h@)z zidoo#x!@ayJt)H~n2&d{5FcU@K7WB9Kcj+rp^`k@4n%+kql$*1nu<|FXCp{+u#Dz& z`Km^QmZ6R=K$NaPg08_zx*n_O7ObYba1K3)HT=>!m)^kn^Z_oU!&s|0ak1jXuazOV zR4KycN-3^T7GS+ngR7Jfu2(L`4a%jsQMnEqlpArAavN?@?#8XkQ`n?Dhkwn=i?~gB zA6t~~xjls2Z3;Hq3~aTfp~2?HUABI>$2J7_+9u#W+f>|dn}!E$OYnPJJ-6p!n{6G> zU5!TDb$H0O5f9rM(PVoBJ8X|(r|pk;#P%t-U*J*Oci5o6?O5zpr(mBt6@SmE3-G)e=XNz-P%prX>N>onUdC%T;bnC*UQzGBtLptM zV+USWAH^H$<9Jiui?`JMcw7Ah@2G#nyXrx_r)A<@ttZ~sPQrezKR(d1@sV~KKGuAA zM=QoB+FX39mE-T)LVT`O;|nc>FSQl;T8rbK+BNuAyPexR@r`yjzJJpmKty{4KWe*i zPeQ6fWhKr*U3}_WgwZ{@D9F$cLQl&X zlaZ00brAW7aRSdCL4TzE2iri*9YmLL+@z!&fKuFMe3pX>hcIM5%y-^I)vMSK0(!JcT+^}ODof5_Q_iJQPL-rL>+$9Cdu1z@1x<9-MuBG+a1p;wzK|9*a7`A^n=Jax z)J80m%Zp5$e=8hWbmLT0{)Sd|x`}1xw5j=x2yH;nq`gJzGFDP-lILLHcjHdFP0rlW ziidaFc7F>}IF@geU}T*s;7%!EQ7fjqnwc6T`0sF+U);jmc8fOh8^tS6TGpbxM^Y|H zQr_20nMDuC?=}II@*AjJTDY;-9n!?Tjxlj(&HFI#&`b7q{Ktka|35Kw`TvHY&E-dU z**xs7W6UcIdofY3Mp|;)&s!yQY*5B^9I2U;i+?!?1j;p`El)mPY;Lo=m3^PGtSYeZ zjKOl(XWBLIp8s|nZ!iVyJFa=-M=8q*Vi7-lj<<7XO}?+wvHg5UvHkxs0cO#Q&0+Tk zGalrN#*B)WlXMTY`~)N~^q)HM^UrpKM1itNl&|0dE>I_;3-!UtbP`4|?w&$@F^jy6 zwSPT~vHLTY9)JtzWL!bnXrMu8q8vQU{pV;1UZY&RPebuNw})srsWgHz=oIQor;?jS z(us6B<4ZK4|6R&MX5pzT4L zZrer6ZBJ6j_6&t>`zT_2o9b-uQ`Gh$#cW?u-1aT4R5NIm+Kb!1RIi>)tJMNJM}M8p z?QB}3*3h}?<#e99p3YZqq6^g9=pyw=TC48m_9ePl{eae~Khvd}lP=S;=?d)>x>75m z_1bK@N?XSLI=V(%L)U5-()HSvbc1#m{YKkCztx`M{`2%Z?RDCq?Wc{}58NK6o9zm1 zviGOk?1N~FeJI^-Kb7vVpFvygg@1IXeLOYTr_f#Y61v+ylkTz4p?mG+be{w!IZ)|D z<#u{q028I0{QPd}3bO4}Mw@fC57`fY7C=kl*dp{hgh=`!OdiowJq$O4#zP48FX%Jq zFw)7bb~}WXJjyW2{(rb);>=6GTCx1UDeE=iF zg5JtzXgS+*daF#GQ*a<%v_>Zrdt%$RlZkB`6Wdl#Y}>YN+s?%1#CCG$ue!JDR`pZA z^vkL0UFZAuTGHt!Q}-YlTtS71Csj8moBoqy3)t6ztz>OS3_rMI+$frkd z`>mN7CL6a%mNISs4pGl^b17LdDn|^h%Ub0lf-BO5+fq|(+i1roivqhYoh8=Xw~pG` zw6BB6GnsJqCF~X5&^}%n!1s*{g5UCP#@<#Rbb@W`@f?ZzLwqZQQE(_N17i$pDxRry zouFM76cFEu$N)#5l<`}fSTWkKEG#yKX)wj z1bRfi64QkTE{g91MGWQP)bpEJUBDlF*sN1q*RF53kd9h^!(R>L+U2#dIIi+%CI;sb zUh7*ytfo|+lVmbKOuk<-@V&`8jpJ^JY$wA>-fB19d)<&UPG!o94cDwCJ^--T2_kF+ zl;TJZ{9}ZBSrrEmSJJB-9TqWSM?sdz^S8Ea)^&mS1&R9E;%{YF4E% ztOuazDyrFrbXYsgUIpt};1txbh>E{|)|JHt^3^~`NE z*LF84&aWM7(%9^oNaj(@s~q>%8<TI2<*jj2lSH0(P9BJLhQfaS59L@{si{h$l&YmY2w1WHtm}OTBM68w(!rj zEBkhcjH|?ckKCC@+|c45RT-%|INZWj8R?N>YIAE?=P+UwskGn=>MWX8LN*{#Vk~h9 zsl$H{@==SO^=oMqwi`Tz2{+ESXR%_z0<}J}Dn52|GczI{VS*+_(Z`plGI#|rFQCLy zVZ66x3U~UEFF|S+VBrt|ul&C0c#}^Fah0EnKlvG?1ol&`DZa|Mag}Gec3AIhck@ekrl3(bWM3;+0>6?b zf~-o;4pI!|SFsVqKpl9@O+4#_u!NYCS6}pYF2Yrgo&r07aiLEMMy2J$ZQRgAQSFJv z)B0uwtC=GK^+sBUc57`?jkIl~DA$yvf31{um!oAa7UA8;_|>AK2C3!hYqlm=F!r~I z0NqeV-71icaIgttOGSvVQqOlBz7tjHp{hLlT+D?%^ObEhXeqGI>9loXn-^93sGS2U zG}!(5YfCTyrPck{K)V**?eqwLp7=6X%D9_OoOA>>|#E-skI zd~JRxVo>0WOk_FpeDn&sF&^UWj~eS6Nare-U~WPy{$XclXJwYJn3ZWD7O+FA@A z$gBM`YCX@6W7VldK-Cy?=`zbF3~I%Uo^=Tnyb*Ox|A=g>-m$`e8#iWG5q!(b)ui`~ zeZ{>mXk9kJwJP0Q#y&}|E?N+z^2QKp|BkI56o97|LJBV%of_ouZFp`03y)SLnCu`I zE`j+7M2+3N*RwO3sn|jcU$R_%kuT6>n~9ji-pL@gJkMLAoLe?yl)LydZ5XafXXh0# zeNg%YN^xUbdo(Y;Jt;N)fdN`xDy?DzLKA%D7&uQbWKfot=G|NDeHp z{%$RvvZeK9#=`(sD%7z|f8Y=NT+{GL=iQt=)giFFb|&!wGm(NPgE{8*a$d!afVBl2 zYnJl3o-wU`(BcjMb|ZCEY@g8U;&~m-!u9QNQo;f~!cMZ>`QDKgHqfp({zX7COhW7h zV1~vm0Ooq|?)7E1YA7Al`)G6Pf_t5Bd%Y2Bj{gB|RR0f_)l#ov3LKE=T&^llnpX&?HpVV6Y*}1pSZ;-)n4W9JbOSnBbjsw$1j43^ zBFoo{+!Jh4&#lDVeK*^Y;JXr!Pk7a_f8z!FwM!T+9#hFqMY%jkXJDwlSsv2-s~zf% zZ`w#H=&AV$8DZ_9|Dn5Oqg`r6cE+*4(DJa!o2D4GqADi;2o1zm4;VvhdZ=L4i|jyV zmSdRnDC50QDNzS&_XJp%h^wtN+ANEBYjwmYs+_K)U2wMsCZz0!uuA8z@&h?6BYc=*psG_YN)d8> z{X5Q_yA)W-*}1%-LY37#Hm+U@i;Gydd_tJt-QlWh77yl-fhADUqwN~xpx%I9DJ*B) zIa!8X@NSecO!-iW9MtJvJiXxF8A!}6e(01<>aiTyo?pylH|J5Yfp6j|uFi`4mo*9s zeM(OjJC(S~;1w=vrmxU+Zv3bnB6YjX5ptr8-rl2o!=lvA*7_SHh7e@;(OT0Kc0qH) z5+)%Gk9Gk_emUSor?s@fSW@Pp=u|bFBKYp8*HGDP>fth=6;&hPLZ*(5L04^6s<*U3 ztKSK;9M9>GJX7bWy1>jP2!5=Da(D9ZV|{OpY)m?|l}dQ_L5L;%Hy1WQ@TAVR#6J9F zJFF~?gQHdj=1Ntz$$b?!{ZmAV`E=PLQb?{sSLcuQwJXr!s361k3DV0An#i`o4B9E} zI9HqBE{N2PsPp_cJ$>mw?*%Crg85#R!EzC^D6y$pEWUxC-l{}886!?>)iqkJKytz$ z_WPrn5wvesoo^u`Ev~bD&M=~Mp792P=DSFMcz86`H%G}k1b)X zQ-0n3v$I^7hcl$+BpZh&jVjQ-i;^3|>tRJNhs+MH%$!5#So=ERqxsimk`}?leA2&&8Nc;kuO})w9f~xk2 zaGhkE!R9rs<8fNU-9rJM9lS>z`Z@K^e7M(hvkl-y7V=XQb#Xse($>BRe@d7lzsO{% zJcfo9$VpujaDOH6$Nj>-)j(wf(NJ@zl*9&LM826OdcuP~$4rO3I6R=K6#%)H^(X02Hs?M}(DIONG?< z1x6B407WJh1M%~ciwk&vc}OBz72g)h8lX}jf}*Ia6)HiJ@=gjmO4id{I%g40FQr&u z8(o%IJnZarsdg8}$rsMso8qTQB=!=snc(qznD?8+ET@asjvMk;x|pV=>Sqd{yB&g< z$AX%BtYff#hMZb@UiIlP(RjfPY0rQhA%mQZ%JCf;@k9_O>}~7S4_Qkt31#nOP=K{* zR(V!DNM7MITxiA`w4C97k}a3nD({#Rlvm&mmTx3uqFG434cb40Sv>iI3fn1^TALRZ zaXz0`n}F`Jz{CWBexY^JBh;IY1wqN@A1{)v(YUSt9P+dfbkRFhtp$~!unDqMG~BrB zIMJ7TssV%_Mm0mk1d@723{>B{006~dIe5i-UX{PQ$vBL?o9=VUrhi$$>p@B`t-O_B zbuf;2UnK{om5$X~j5X2N%$^ono)%VpoV(3JHo7%Fy&?JE%GzRFP1~~b_ZxJ4oHZj& zlPny|%Pf)~-P2g4z0!3wr(RNZW?v6A@FgN1tLw8E$}Z3 zaNbT$b(}9t)_M6S=kfD9s#Fd<1hT2d=U-2L`(K7A+8$HuAw&f$>{Jh69z?t--JhzN zAU!D|`AQr7TWAT^WDPbQ_*Wcqp`R+*FHJw1gAt&Z``&0VqWlAI#N{1LNo$PnS{zL%O)FWj%$Brr zN2odkU+hw09_6 zd%)Bgg5Q@-$gnLC^SxdHsg@UFRBEZT0Uy#BluWSvn{i13r+~?CTwv!E-qYV#hCVY# zjz2Hw`_;pB2*s({bwtVaH0CMK-=z{reIJOLpAoB62e`k@Ss~H5%;N*3LO!Hf1?-f@ z>zeVLgI@oLN8T_NsTfJfejtWHK=NrowLB=pX9EcGnl}dl<0CY^+0NUn8WOv{p4~CN zo8k-zZjY{mY0;!+@<4(5l>6P9`abX53MU7M*)fxSw=e3nhbirR{oCo&$9>7}iqH?h z4VfK&_|B~9Ed`0vTcA$_2bM3*=iZe;lR)9}RlW7hH+@a~%IlLp>#;zP|LGl+Kj`B= zEbxm2_(_0`)6H1jtRc^DK=@4DU1Q!vsWlQHl@Aq31T)@D2t3*b!CL#R+7*18q!9cP zqtuq#*K|oS+2Kd@^WY?5svO;iHFC~`LzelIY}P?>L1f0SZN$p7uA6Tl9w}z_)L@0t z##lHs>h~+LJ+97bCk-s9;pXQYSE>)rRZax0BfiP}3)Ce>e~}wuijD}&sUhbJnf*eb?lD*_Mlz2!@|0nN0qMVP;F4thq}*_EuL%+ z?RuvoFZN7e%PsD`Iwaoq8%dXnOh@nD6H<+>5Nfo^_$b1iSZY5oA5W64VO3v!`0Kn@iLStPLN4{W(|J0QPpAZq$n@X{Xer~ z`(CcUhXPZnGHMBNv)fsLzP$3X&9geWIj*$#;A1DSF=3zGHDXskx#{hR3{JniOmwa# z3+Tspu+*gaXU_LmyNMt69L7g>A?}Qvlj~H9L+x=A!k>LrBbTwHBjTT|_THM9s9BPe zhf?Gs=U9y_M7_(1#L^tga&WxHOt^ZdK03L7xdGBUDk6}v!uWwr?@(NMh<-bSvZ89G zg6vxUnX}fravVq zE}&$^O-JiZR(bI`NX-d|0yiM*N#n_E?b^JM!BYFqlHO%@v+`H4H)l$Q%x?`Mu_E!i z5`pT+E9Q9c}|6c@C%jT{vF@^g?B*f;E1%0akMEl~z1 zdK3*I687RmY<49QW31PP)^YSrCJZFqv7_E0jCx>w}4 zmT#0)oqs5kNSWdw$Zy4p{K8Pg4Dtgb0o)JV%j0a3aXDqdubk%(u>AKw26wyh`gPGl zPRUder&yivLZcHq4ah>HI}J(mxEiY(l%nz00b<=BsG~nY?+L)(2S$x^0;XniYZxFk z8|D*9KR6|Ei9(!l6&hUv8y^}4kpPC~M6>G_G$yOZ1dHSA{zNikIKx6JjEb1R#HBPLbY@ozr!>w*zhxhwkzWERyBhNBE?Q z=+KR;iqUCQRu>HekTbN-6##wZ2+(jV%emb8VswsVSHs@5L(E!tG6;&rfIjdk0|`GR z#^0wnjDdaHk62AUFb}LHwTB;qXJTg&_(fiBP@|Gh){d5e|j_q9t8BXiym* zRZisTP=Nkpy4fOZZ#!q?1^Kg)6uH9D8I{y2R#4puKMT^=D0O>GuN?@Xm=~Nb3HkOF zSOSO666<#g@0K6>jI@SU++mJO5V#*M!rML4_mN3VILOC)L`k4Y42xT-6VZsAK#s&9 zcju;T*}UGePAO=jn2wsSH}aY79-19)SX_p+E9qE1d7>2LTty=tT^_!iBq;eZoS!$}OsR48s++HMHBsGnw zTCAM!Ev4bCp`n;_c?VeGI~?ihr4aPcs0D3Xm%`k4o=ss{BrSzEu&8x?mhM_^YgeQ;YIrgzj*#eup$^l?m$5s8ILOgbRhfqL z5Fg%s(L6)R`9(q0h(>Z@;L&=_YiL+8TUVcRZFyt{`q{viL0{Jzcfd1}VQ`<~YBzlM zUXXwuPGK5kRykQ_rnJ!Mh+0Q0`|%RXodWA+@r&+|i5k=UpklhL#SOkQ@t%_J`iqud zEA3qiUJsCFB1peh9SzQvkj)KMUn;u+)#ufXEYE-b6uje)9FOD-EOjC0;)KJA?0qi? z)!_@((KdM1MpADkI@EFk?t45EP4NblKTtak4*&V^UbLa=Y+f!W~(!8j3N81fVt$Xlq_c7Q<>hJQr;C&JS zlW)%`-6{NsZ_iNMGoYRN2iF0H++PxtyR{nqh+!x%zYd!xj1?)o-~FW&e@`-!{qZf! zF9Spu%b=_TLEQPiU=Xaq#YfU&g%_zynyE^D1`NX!ijO;HngkFb!7C^l3O=4#T)r4V zDk#A5usNJixml*`v2!^UHc)Yu$sw{X`Tby})e5z&B&1vzNe;J8%cJH^Gg-#$!u*=_ ztK}qDKjJ$^IKt#huZXOWr;{D4PuH*AxCFc_D{?R`G;5#qEB4io`g2Y*H_H2J$p3A< zER`UwSV_%~pt@JFsB>S0z_@*mw$h+oV&bKkOtD$3De=%vq+{g3uS!U#57}r>)Lr1E zCPJOK5uu{+2U}h^fIU#ere>370b-}`s8OO#Spze(BEIN&J5IHi$7P`a2rK~d0=%TC zfLHx9Kv-aX1t;IxDbjUAo%up9e=uiF))1EVNa4J&X}q2-4)l~bF-HT|ihp1S#mJxGS#^_&d1PGsCGnqqbjrKDL)jKM=p@NGEC8 z=3I|OmvWS84$2`dzEeDKKz@w5aep@feOs^ky*WXZbLSenjVT(9ejJ@jk~9*p>?}d^ zV+pCfTlDCy1^a3{wy%av`r=8-QZ=|$rmoweQDb2#nP|PZaDb_tPUf{fF|E}HXUTtUSBRER0Ao84GCbY44Nr2LhRYJR4S zo6Rm0*8d?YcZEl$kDe9mjlWLv9Noh&{z zL`X8Q%TAR)Gz1~F_eO?1l*DZvHMGyIye&iutX^biz{U-kt%lZL`oM zRFaG_R1>2e{58P7S0r>}d4!-4D~Bf|2yJnv|EMj+rk{o+N`{dxvQP~Xj2p6!OvV8& zS48bF#(Th|{D|9ffoWf!V|rDPueEnWlCMYFuR;#-r4bSB>+RL&E5c8&aiuckiLHLP z&xZbFm=l}*{gSo1jtewlS}$1RYg>k*U+&wj8^I(@+^T(6uyj7}b(5S}$$3xZizUtZVpG(i&DIhxdycVpGDv|pE73^AlOWcRZnz-zVwg2o_neOM7=vI%1{ z_sPDryK}{gfLo|qyE52`?VQ|GID)4@reMe*xu=Try!?=oYI zNa81v=Lt$35cCs8`_!o}OEYuG6_wR!ur2eSBsOgCT|{{dMFywN0pGt`MhIbA@FdFZ zujavL{Jbg^a~sq}gan>RgNet2&U_>Si5g;zqhhbp$I~HiewDzNVU}6bWWyAu&H68+ z(A$ZhMAa7f%q~`Dx(kulNkrBT!W9h7Qc1S!!Ytu$K;VEck1TsND&mkrX5g=oBynvg z>#P>?ZitN>qqFXy`hU{mlzv_POd@9sOHcxW{CV_7)foa*i&Al9?ke@ITF_|#>E45> zZICRHx?8wyPbrw^S49HGyc6x zLA*;6K!gk?uj-gD@hmj922JC&|+Hih21TOHt@sa-AxMKZJbj&{m1X>3<gc4mez)gSx7TIcarRrzSLWO8e9H_U;tr|Dyg>BM^RkAQD&SS?q{_RD zj@V35u4Od0Qt_AN77LtXbgI5_2R$!y*q&3~3o$#H+!PnT7 z(-0Qs@0-{$+5WEix{9D)$+R2zWZ>qqt#|^oU>wbQ9GrN$DL4vELSJ%d)!tpd&746! ziQG-vs-ER7CvoYi1QPp~FZdoHVzwzk6o;5R*ew2<%m+*RV zSg4tx4bwF5M+j{2Vn~6jk+@d`p55}{VqM*`fw3SX{<80D5k(ub3?QC--tu-nBJSk4 zkEbYTA*uE!<kadEZlxFAHD9FfB@nJexvhpDMp1znDR)pQ3+1E&4MP z^1!XYxfZ5BFA5n<_1#5iR-~v)?W7PFb?YH}Mv1%k*ypiw`xgzU5hURTn_>!#gk@9r^rDVm@pf4JA;dj&rx z-Qu2e-;P>7lLd|WMk-g{fkLM)_qP>gm#8%K>0a9YFnZ1^GNcG+(u=mf1F61UvZ6OD zg^rmpr?jx>5HG9gW9CvSaOssaQxqXVqj_zLm8i!M?9u48bb=ZZuBzu)qa{jq1+fzVc*#! z_(M#i;U0&IQvGRX#*|-?)FOxs_P@?_Pv{QB7Z^_|H876eD!3qb{U1~ z3ygoIeU#<9HU>7)3(0jD|~#QZzL}CM+yCvFmx3=7Gwca^+PTy2=sNNwl!&#lg*GFY*-xBsu!!h-NFlm(sV1;DKTBjbs5ncMB3C*7W=N9(tra@6b?u2dl}y5 zHH5TQ$yk)AmpbScjaIZ4=wM4Y^0L+-!B&A8A;uQ~W#vOZtu??`+nT}|#Z9vn=P9=N z9G2TZ4iM4RW<_tY3V?q5hM;3;*if&;D3`D}_(t~*^(FAOTBAF*j~qGU*$gRNUv3@| z1o_|%Pd$H7Rt=2y543*?+RAZnMmdAVz{v4Rtfdn)CBc<)$zrl_YhS32)jGbBV4Chh z+Y%w2g|@6=6U%G5uL+_D&=rm;3p-{65zt(@1KK2YVB3YWHU?hgS)AHddRQ@4ijH;H zwAlqoeG2A!kTG*|q~{6*DDbGJ8&r_C3m+eY{deQP0v2%9?8MjOX;7qk7&M-#KEuu^ zjP5f2x(i^N&940LaK{KwRCOZv0s@e3PBLk&KTE1eCmgOH|fa`3` z0m#V&Enmq#lRjZATuEtzlZ+P>+j|FTA|D;n-2t0j)%Re+0^!F##bD;-=Ns+!>E0fu zzWb5C@sDR}Jk9Jbo}e2l8mwA*@1R zFbD3vV<#0fG_txyeH{rF80O)Pz><{(oYMaxCJSeiOar?nY-U17jBfT^dA7tz!ZR!* zmb72o2?#KsXZ$+X^+STb`t?tEO5!VNUZRT-#z8)q%=U!wXWaRJ4(r2vGk+WH+Wobw z?9!EkU_%%hVAJe3!;+t#cyu3*{Y*g2i*D!ioeSx_GTm)0-?u;?X83QToSAnLP)s9s zq;sr^6HyJ-;xZUOy~Y!QS%4{=1|O^-PP*zvhN+>NKDPDO^t!h_NE|hqJLy$%71@IO zMiGceplI{ctMf}sV9th0>s1^L6#Byi(FJ{rpX8+xzV7Mduo`s2McLs{zv*R!bzW}{=1^-fbX3U5yJK6jv9-7QxEN=Y15X+^SN_@eJk$V|TK zu>Y<^WCUME8vh!o3Ia<|_IG~84c@Gc7a`;yksBO;;V^l)P+DvZgtz ztTZIJ_gw4qRDo5EtL-H?9Ko#&+8lE-M*J$Y zbziBatCOuTF=+zA#LUowF&O}{2v}-C2Q`AHKDi#(K*P>nJg@74BlbOBzVBim*(cyY z4olmBHU6GR7E$S%S^wDf3!9}Oy3aOWMeg>+&@a=Btt>jAMZ>Wo)~#!rF^5dr>b@eL zA+GIsG;6%dZ4T`Nrsv}_ZyaplOJCm$J=Kw+>!;-+rAgIWqWqQHrO5{(*HkDNR1Izj zTObN_Rn@#lt476}m@>YKl41Ki;oQb;EE&fs`1F*}=N&m43Jt}Dork&t^F z{>YiU(BQY=djx=@FMtZF)}&2{6Iq;#I(nEX z(LRM!4b6jomvU7G53isl*6gDGOjb;KzjhZ#0oLz#e)n{1>= zLX(+pEJ2l_OI?mq#->(Et%G;rVf4CDTeJ}$V4u7YQ`Y1-k}gp3ay7Ww!_ye=E5G3M zqDi(BFHJvdp+j(QAp$lCp^3}bjIc15&WFhe|)S8mT@%C25M0 z4T}l1iJHQws~m&hwmZ1=QZoA-7hn5i#b%%{{sIUKi1<3u{(!$ne`J*3fxdJwGQ{fJ z3-=)^x3=v(qF#1_7I{0^!91yIgIHm}fU3bDg1v8^>~ZUsuK0^i{O0$r z=NI|!Gt)kdE>`MR4Q8h5?HJcZ*9Uu+Ms7e1!_H<9G>9FHc89`pDc{1W>Sg7l%B7fI z@4`wDQ4+`u`oRrHkQud`?a&6Q>+Hyv#M9OJ@J#y+!y!0B$#q4LE~O1+EUMnw@D&bC z%ie|mSE)Ptm4|}f^)|QOG!_9@R7q%~OTD5kg}`(IU476DwukUmzuD>W_WTUP{0txt z#Rk5GpE%ZFenw=URNC*_Oo@yV zB5D?-1dmn$bVB-xIU*Ykx(f{w2U;LOPKOj@*HAOWbTxFsnBwV;m_8+EALa$VI+3GR z?{SCP15tgVq*d(EP*{$>+Qt9WRg4l8+zU%Wi^4ac=DNvu{hxsdzS~5iDnaoC37!+( zgg9Opu#rfH@GwpwRjcf9LMWD8qkw$%B%axCZ-W;eBun zKvxUAIvhHm?=uzVhdAIoDFFgBe2vYszgk=!OOZ9l)FP?mCFk%)X=tvJcjc+*P{$QX zgP0w^YtysvimvKFh6La}Zi;mMNf5Skp1y!mPr=>4E^S_%sWx*Iq36HiQT^q|~>D7oB+SM(`+ybvg0f zP}rUrJFiTNS0{h;05P{uB*SL@`zZ#n%lm>-Zi=@=(Y%qH?x=Qq4xLdH*BB6O__7IS zyITb>vwYaqE#iNR{2<*pw_?S*Cl1$Y#&OdoP^gC|9MK0xA`~@=N z#QRV3;>55U=g;5O7#kHB$;=p>)^`IXSeiOYY?Z#YCFu1z0Sf&YHn{GK4Wc^?yOf}Y z3D;5L;tI)i6G^G$WSgPE#7t#*g+_K2RH@X-f2EhZmb!X{HD%t5auB zU5s$hEi;oUfrTh}r{h$Um@`=N@+!kRbzEGSy`LF&5h_fj6%6mK;y- zw8Q&cDhY&Ezzs_LsT}etEa_wpv%RY!yzkMoEBcc`y~-Nlw%L`2=U*+QZ2M2|Ho3-3 z%orAT#nJt(3MO4&!i-i+I=Mpeni9K|U!@zpiuw#)tx6n^QXfC`M%KkiN3=G^n$W+rWAsWx;qI8UzoI0~fb}t!dTV&$nFR4%KG8R;pvED~%|qhq z0SO9veQIn06CoXNDgzo406v{VF^V(Kn`7tC2gjj^%=fc>PguNhrq(@FinDZt4LtKw zy{Te^(HZ|GVH4Z*bp|g4vu+#vJEI-x10QHRM8W-2hRo^UV$%U$Df6-~Z-zY`)Ta?( zU6wV`C(Tj9#XZm3f#*TXEu}-Xg;m0c0Ostq+$KiZ#tSuMi+bZNa*(@50ZV3ZyMAiZ>~w* z={6;P3qE+G^9{oCxPyFrG{Gg&^r|p@;sWB$e2nMU5&l|gb?(}6AX`k0X(VDXxZ7!; z*T-E%n-rov{M&%1?Ct6u!h3+J#&ugKo&V;44Ot5}fu=+DBYWVaPlege$&9+pkO=)n zft+wz8vl^GdRXd}D31kIdO$XA%dM&Tdu0 z9hk8`Bm_5gtsw$4+Ip>SZsG>cYb0PCQ-spqcWsP21h-zsFeFQARX@hjoG%M*?%K5q;$9q?gZ8A zRLh>8-z9n#W=DkF(%Om2R-3~IS|j!fN19O4G?Akx4=@E_iv8~w&v`9*Jf%eWFHMJt z15(o@PLc&=iMEG}^>wmHYMy`rF)@%$w+C9%3ZvKG)8)*+N#IP3w6$qKTP@jxxhzdD z6#Xi&=_VSfmD-bXkz6Zh~tmHT)km|!u`uKGJpd5b#*l%Hh-r_w&&-e~V6 zs+ZlKul3Z1;6ZRl-q12J8RY48_YHKe+~8QM_CT4*nZ43o)|Zjn{Skqv1Z-0qQ}aj#bZVEX~^RYlmqi9rX z4N`-M2~xaO64(cNlFb|kTP8_FLd|KfH34B}8EJb7vHkO4qbIk2*WhlA^yNkO4 z)&$~@hc9skvw?Jd4zrN-qaYh6Zu?J;+orXyy4=lQ4K~ZKx(mjKtlsY7pDwg?HWX|j zM-b$^W;D;-pn9QHb8b$-zJIvvGyV1ZRyDar2i#$Q2CS2va;O5v4sts%=-b1MMx5f% zG%Q?%1r5}=8IjjzBep@jK}cvbhW{eU>DFJ{8nw1v&>}wd*@c_)WzNTw2LBo*XZ3mK z1ILnV5fR!f2rQ%j(3uIKE12CCdtT2!eYR#67pg$Iy7VI^JXpOM=ks>JkM7(ZV#895 z1Yn)%(0aj7tC)b!!o84ZBoOQNlEJ-BY-y0>>Nolw3Tj_!v*UR8`9|p95zj&XIr3B% zC`Ix^3^z1?XY>}cPgB%ZxcRzyd8M-o(-(uv55`5S)1RjtMxbu{X$SIsnji_LubIfUk^`Y!iVTT(Bc*6^d%=)>daC5s+}~D zD{xI+V1moT8#LmE%0m>DT-upi$s^8Y3i8{SHL|puRQ|vj=`Nz&8rLytAybd-4VSjK zM=%HO6I}sHCSW|__V_M}`lBgB)ulb|(gsjAL|^j)pXN_cwnU@&g3j|3?dBGx&X_HH zflCFYXE4o~1Kp+AO2S3P@uD51@MR>N3Xz}`-~DmEpth*5Pl_mnx?^)tDNyTFpgr#F zVgh-W23i}aF~^C04WQzVa!2#sFvpV98*2RnmPi23BSF<`zT3|5;jS6o%;FLs4Zoh< z0LqbyOO}=%j-G!Hf7*6VA&Ci8^e?h6^_#}vXx$Lz#dt%|WkV9kx2gBbP7oeW6As5F z3=YTsiUA^#xd_i``1MgYo}Yr@GY*knxI7NYW;0ISK5_R9Mu>X5sBhHi*VwiLX9w5+ z>Rf>#31{CM+lhc!B2&&AMej(S90hxVpK_R$Lrw=?{GdJfGYTzlVHH*G*g1nGr7sBM zudMilD%dxo-oZMZiB1Ig{R4rL7&6|;B|k0K#Ha8{6Zcv{bB-;3V~bydXD8npW7+Ww zw04R8_~|cBb>X7*MdqD0-!0@rO+{0(X#}9G@yT5{Duqz8QS3rXxyPe=?YzajqeAKB z)A+#j>rj{40~}PP%JwKopAh4>XMmC29DnUSvaoVu0fY&zzVH&))^UOv%Tl}tQt9Mg zMPCilp}eed>>oahoJ2m5KhD%NJrfcSjUDvGs}#^MV^=?-;w81%!<8rV7N(X8IDrLK z7liH6y);W0te<>+K%VrfB!aGne?WC+_=b&lgrHwX{O+%ZlE-b+mGIWRjqH?*T}ipi zZI2n6wkX3$#~izAQ)XZB#?VqTRE)y!3}VvdG$yv6ges08IEQ~ngiEdCcyc-_up&Gy z^?vjJTdL{LPmeujdbToF`l>WNhXcrx@7~<`Ou(QgF1*n@)Dqhqube^Zezzwncbq|m zfu1Nj`73xR}p>78ofJSo_YzHB>;uU>XM!s*O#VG=_tPP2E~~ zl@eW(;n@!v-S)C3FRQ0zE$xm!w#_bO^IzR*(9kl5-8F%BTaN#v&iu#k#~V)D3wFHk zl)uU5^ZUq3!$|iZ9GvXrsaLa@_fKCXnCGmDtZ9;}!izGL8fyfoMP0T+X>Nb*O<1lj+uz)D|IT|wO|ys2kPl{32h?_m%L-q7+4jKyXrk?+A9htjofUwVA~vj<&mZXEdn5qN5HM zUZe0AkslnMqA$NVBh{?VU?0o?GN^Q2K-HP#1$=T31T@n?6u(ZF)=OITGc@0Uzf9rrB+WC2Sv6;7x%(KYIxt& zu;hDZpFqQHnEDH8S}m9O0Qf*_T?5(1xHbLAi#gM5codg@x!c8?CoRR{y0Z=?ejuUYiv<4;q>Z=BI3=vYD5EuLf#2u-;~` zWzxUuccfV4pY9O{f}$HK#OkFUJ=WMyd1qj5cAbESZYnh{rZ&iLcC zRH{Fs?;aCJQj@hhYhh8lFJJL!!az65GAhuoka&jk)!9Xz8*5@^nS5cZir78O4;2%z zrL%d|lGCE6#eepTq`BNK8^t^sDr#({5Kf_^6KYa?Q5gBzPI1Wj*U@s_tf;H?`_-&{ z5h0_{oNFj*%;7&2RUKxDbPDGP6z{ zCvBdGd5uz&R^Sbw!W3xEaLGl*PkSFJv=pFF!0WeaGy2O$!{jM*IcU@c#m&5Z2){}7 zAg{RSj4b;6~0+(ySX&_?fsi;b|7HPwd_@ z0~f**&)uw8Ry+Hx0c~POfeD+NYEla7v;+Mma+z5ucC-V!&xOJAP}IRlX?V?^RjGa* zfCFNbZi~m=@Koi*0M(>q0$C2SjCP6?nX;+}!Vht@yNU)Wj|SYQ-Oyn9g_!|L=4-4C zd0z)FWLC&6<1QrKjc%T#PS!urjQ>N`IdF*?C`~%a*veQL+jhpbZQGtVzW(Py*f^9)><_k+5^U^-Gu>?MKq?!GBu&Zk zrsPwuu&S9guUnhFw&Ra~H`@f*f0gdQ__kVTN?~aAw5i=hNJ3| zci~fv(U_8|N~tBG*~Edg9Q}MqKQIhl)TT9tV|OyEPgqssO?mGljnuoHeoVmD?+mnl z=W7*jbj~{?1A8cPb9o8j$NUKQb6p5$ux`X`X&yKqNjt>7R?z6H5>GaO9^PcZ4C*!T zeB&OSpR$E}hwn1|I35Mq87#;(@y$6qeNzN1&c zkUAmEZserqD?VZ8J6s)I z2~%oQ$C)dZh+Y7uAAhM#u^bY|4G5#LVKw7BxRQuRyB|;Cr4M1}7}yRFR2Z=}<5T|- zEcAj*f|jZ$Rp&kOqOBX1tJah;BI0Si1;ewI*40<4Rk;;F$#N?95nl|3PjF7@j8XBM zWPloX62RbG^Q`|WSE^cAG!!WgfJiU#FVnXn?J!TVuVUsvWF(_wHTp|!Oe4jaM{SvB z!5vntG!elpHXJSTK#)z0ELliXRUYlgqRwG7gu7eGsL1wY=((ES)gOIQcW#b=2 zgnQX~fuZsUs;OKf-NAS!gUE2P{6c?Y;B^_TR{oOu1^+OVsbkLSuDl12- z$rDlYMAHk#s-WS!;ry_>x|K54yJ6%Wjg1x0HY#sC?ht%W2HEA!8Su#{firCsdq_9Y zbpad1tlMw;ki!MRyt(@X`Zg^X=C8i2Y3b$Zoy5WBy^ODde0ARE%d2(7rFJ&CD*_=| zJm{)8N##?4fX^L%ik4+`6T|CO-|#Qle!_*r9qD-jY?;=vw>BiE@?EWSce9_?swM8Y zeXeG8@%ADXZ*TZ<0Ph(PxOXddwB4{G?r4+zaA11ZT>E-^(P5&u*?hzU85jf^zLSvc+MRS zL9dQlq%ELYIMwZbGo)hHS7ycc0zRfzNGNujVGyRj1Mc5?og_w!Z@HLsgF0&-BqfM? zwVq6Za-+=6UuMzY9p@Gbs`ZQH87q?%K;QB4%2;Mzb>Qh7k@A+3h6@)?C+r&{S z^SA68*DI+EKVP;}l*4~fx=>g)W}I$YfV*#jS)<`QUpK;E3ZbtLNWP#jwDEtH?urI4 zm@|ur;_z=%07Uhd-?C$JCQ0G?YuEnkl)M4lJ&IFWrG6=RRuJ^jmlhm99Nc&j+x*B_ z3jjw`nY81Wp(mxqVIL9{e2B=0;Ces$k5}AppN@3m%J}5Ki-chEu;)&?ZmZ$PqTW8O zF*JYsK@i6G@~QC;O?NunlNsmElR^vXBDWwsu7ZLJXkAkarBBcA&+nMDf=p&Oz&?G? zbomfTxY=gq^UA4>%#Nrg(pXts<-X6kRv3?B@qkCKwQEs89j;X z(ZnbQN$vaM`|-2y%uP|GN2FZQ8b8mRI@zR0T(+SH#-OCw1v7B(Ga;X&Zxmrg5V;)iXW+M@6-jgh6 zD)yH5)Il+-ud`)YaBY53ZSlc#QIJZ9Y^8(6VZoUdG^NGrc>7YFgmPhKV zpW6=2xll1qp)$|Oq~eAlf$T;c`JJZ*f}vK%mLuCJ z@SK)#;2edkto-M2%C)*`Th|woV@g+63&MN^?q2}SflZ%E+%vW}qc(^*&~Yig$tMekT@gG>=;#^J?2+ zhj*P6J-*Zy^GVz-;p=~R{ujx;P`W6v-@a}9e^tkFmQ6VHAUZfd7;WMRx@eMq>488c z)k=Zj0HQxof5IRc{DktAv!YZIKtx2G$OVlNo|jmVR8S}hRj&HgOhT=M(R^TgA{Q{daB!)3?OzbfRf zbj|jU1sFOIc6lAwJTWtX;mNBm6Hzo2}gfJK959^ra z@B|IVJ`xOjA2V-qM3@0Ckcx|=G<`|Abg?>|Iin~c6yJfFn{}Svki7?2pivG^gOx49wZ_91TnHcP;1!1R{Q*H2gC$e zoIrbS>!J}Dj>~Pk5oL7x~Ust_rQV|&ZE}zQ7P?neOHnN;{|wlOt=Nc@T8o&D$freT7^1%!GZAu z@rnQ@at!6Vub;atvh>d6;AYsmOf)=)!bFE>1Za?lu(>O89{1T1>?_@b)M)Y6kOj?7 z^+XmTR#h4oJrt+EOkATNie`O^4)3pXp=fNFVZsFvSf)egzz+0Vi;ZcIxGzx0v0{DV zz}-AME^Eerr@hu9$xAY5?uB{eB?9p2=6ZpInmuV5TbUC5vx)8=!>6-%<;>L*Rf`GG z6>J-7L>NtU^&J~Ezt`RqR@!b2SVq^`MeX+W{9tgy?hI&5O3a3TbM^YgP|RukY|336 zG$v)))`l8fVkU{rc&t|{W}~-T9`s~^RD>{tCb?+s&>PHlVM!(>BkMh}VFD`Q(NJ;h ze>vRe!qf%vBmU=2p&Whml|sv1#rs8SSnqemDYTKLbX*pvdTv6D@M z8*bzb|0oIJsW{WJpq;pY0>FZ@S}oH%_l=i?u7;b>6+)=6vNZ&@jbF-XGiTNc+*FUK zmzdETyHfgVJ{}6LR*zm9h)JY6TP>=O$Z?}CfCA&;1u#-qfuv+Ctxclee1v!A8xPsRJf*duvU;xGx`0O@!5Y{cLtPus|8 z%{U&kzi1I?QGP0`y4!(G4EDr&9M%|Zm>{u)9B_+DfFw#|et+xaX%4-67*|zn2y$+!iqF>sMdQ>>1G7~i>~P2b#36Qr z-Iqn^s~k76?vU zyfj!ggETVr65Y0jysvuH#@R+5@*1Ju1-wi_sJrPgDr)b$WJ_Pip{#YniF`2@SbeGWRlTOXBGR`dEyjOuci-i-6DY!JkL+^Y zyK-<3vHm&M2E_h_lfk=FXj9*TqAwx0mRjRO1_7RRGY2TE*djWfY2O^ z%_aCuU^J7bND-ISau>*Kxj4*?78Zn|tCln*?0~#aMr^@L6+xpjU3+?oA!0&MCZldL-Axe$LzUO&Vss+Z7M8@rp$$V4<;|2y4AD z)ezQ41WdB&Fg{?IXfhTpqn^m6W9np^x?oUlBF#)W&4ZKQaZ=d^%tq@o_~X2xr*Y;O zB(pI5_dhbVKu4m`cYDuClOVZP6k7V;q}5kVgX4N0UEPwA=p=`vER<#|MzZx*qc`b3 zPKcTk;Awg2k8)F*xZWjoFz4gm>PWqRFf7j20`Ql&P9Z{(p`Z)?(CD)Xm6DZ(J|=kA zzKhv^hHBvt`Lday;PIa~4S$b!zMOs`*wTsIT+-X=%wXa$l%>qYWh_^`cv>UV5$Q;wI0<#V>uC)&UBj3$xZtLi5Ibh zqONYzo-*oMTJ7{Xj+SREde7OMsHwiP3_#dr#LciC#)0C5l1*DFu{@Op3tq18=1$F} zDC?BM%AcXxue$Xm3L1}#3qjbUpdjP7t9$@v`8Nx!Z8fMwrOWE9cQ>oTz}9ubY;-J3cOi&rDpp zDeTyo3>huW>KTP!g9|urqxmc_&kk|gLG8cVTK<0c>^7N9{P@^t1(*{)?7Pq0Ojset zzhn$X*O_8yGQFVsNLsk| zzQIxn)2n&BfLZJa;0D#1unUlj26psAhUW)NC05M`YCSnP5vL7Z3>GVMr5G%{ikNN# zsC#La-lAL$i5goBfDFxX>4WsJill`%O5MV&##X-7g7#TjdWDCVbC3S#EDyUE+mAA@ zIahR` z009?$4t|3JzCvJiZljj{IW#;Ss0ty*8cu4`K5te1P(N?kb{2K3AST z$Ab@W-GdYNW2O$M2HB6^iq4<-%=|54u698a=ggV`CUe(1dbA-!!Z(v`q45E$bUzL= zK}6FWJkz}IP~&|h>`@*=7bSiig$CXk5eYa@tQw;bI-p3k zz~R;cU>Oc{8=hRbwJ=9xx-~MqqEe5!ZNvU-e1X~(0Rt}!i1!tv<#92X_*L%GA~)Ew`A zMj1d`;+=jWTBuhyA$K_T6a>h9?tT{b zjvGkx-hOTYpuH&Js8#VfYWFtqs*Q3R#%?mM6s98iS~ZxP9qq7H*oNyE2ZPW2U%w+v zTW6Y8OBrsrbK@Nh8?e(hmM;g-9(B=+ch0hMtiJR01p}eE9ixtu0-TRCpm@*vI$r<4 z3W-+2dVDIYl%0uo9%ccqizY@!1>vK^JDT}rjSBAx4n`u8rSW+UFbn6LgQ})aQq`W6 z@nA*ZMlvx8r#^7>;k-T%hu7 zs*R`yUnE^dyn4!ZVGWFdFY5CSPTU_Fm!Y!d&r_cD55u1#f_fqg<#IQWOgPDmOxF$(G1;-)es_u~plF37@p!N&jbA zdu8}~Vhq=%K!#-+(_t$DAi7jCGiz(L)j3xq9Hf%c4n|UG%|W%M(oSP*LbjaL;NQkK z_UI_(ni-r$j|0Pr`ikMClRa`VNZ2020{hF7V$OHzT)hq_!#K=)fCO?(8=7y4W6d{< zDudc}%oXyD(C;`Wok})4@}0;1<;vtcUf?dBe2k!-pQg=wxbIXMU_0TxB1LLq&jhi% z$cbSSJp#s9Lnnyp58S1 zeD)SO#-|tgLM+-5WzU#tgySP{vnBM)LjPvEidNp8LH1Z=(IWT6O<)&&X`rSs=iW?!)$MU#I#5H0Ex^#p(&(d*0i2cdi~ z7$>Z(9e-RgsAL*?G=r$26UD7u)wT)eUlFtv*= zFTX9r;C=T231~5!$zLj_aqCiP`;TN@DImjFk3Vi5VQ*k89!S=7XAqxyr^MJ5L5Cz6 z4jGuVh)KqMBvU?4zdjHQ=bxdI7_ZKP9~>0Y3`5Wl;ATcqwpZMLq&H>}=)|)LmHqh! zfwc>|hDf#=#GWd?0hYh-{hqu6)O_gB99b5-1al`y#vC#J%d4KLh%h!iptjqL9SEle zW8F)-ywtY$Rz=m~Zx`KKlzft{L)@;4fBtXGGd@ii=_?@uxqF``P6_#tU{%6NeKFMgp4Y}BMyB`|ALbb|&;0bGF+O?0!0#s=pT zPu{G?PSq^i6Hn``s|B=!kEs?W!5?vAz?5#f=OxG1%Vg6>*A-XNQs?uf;6kl8a)4^D$4-@fepRDI;$zYZHVCd6w66-<3RWoy zB&KncWI}a?2Fe%77J=>`1D;=gqh?`Hi2A2_8S+mvR(M~`rZvuUF+y5$d$YK|a?K9* zzdCu5%;48T+ZpE3pon7`V(G?(ncn&ZPa~J|%80boaT^!pl^sWz`7o@JfbcPKSNnRB z5-6}rHD-yxLHN-q=q6BPU%Ptg?G)V50Axq^-z)K?m%4~>t2+C6{k@Psp@J3y30(wh z^Hw5hRBNeFgf)Nc6#k~p-<*r9?0-d+R&eia@qZOOL6sB%RUCC#b&$wfA@!id+KWs~ zuWzyP=pA3j!Ev-o*EH7C0!ZavqdgeVNJvSG$<3PjTnMpl43K5m=+))Y1<7Gdx1~;( zP3Fi<;}>x8-7)Q7WiXRx=|pf-pnoM>^uR^^h4$6NR*+WFpr7BjdtjXFwR>=YC$WOB zuWw>Bk{(yMJh|LI_9Ag#h0&RPqfw$D)7&xpwVVuZ)j&VgyKV?$4rnhx-BBl#%V|tC zRewW|b8g#^o_ZU7O>N}t{Syv`oMV<>iU?D)+)em{l*~5}ON)>^d|CUiQvkF`;sLXC z#&$hF;aV6YvznS;0j!m$AS0p*{e% z7vmzkzo0Dl*a-HMsTdC}&Ia41B4cB%mf{kf$d$ehCYqk*r{V zMZZ91e9DiY$y6X-vSOU0i8%qu0j@~k-VjLB90Xi8?Ncbg2!;c2lBny~+V1XUMB?kFO2T@l;Q_`J)palB^}- zMwxifsA1(7UlIe0u9-1KSwQ%eSGI+#ZL$>`5?ppLVWvvg4{e}C_v+8_&RMR#NPR62 zlB%MF**KuQ@H++l!Tov$jfxy>)=lQ4F3Am2(FiZ3!XUP{c=n>{9?( zvf2&HFQ1XKZ$si>MX+-D=3a~Tq~*kI{0>&qL45=w9zLSw84TzXM%+F9>Bnbjs3THp zaY2tmEM)4Ow|UULMeyPBu`4T!tzAXUhfqaCTWosqb(;^iaw{+SY4fFDqI<)H-VPN+ zZ}sb}cz-;^4E-`RytA2T^4ZF4wMc+oBD5fbWXiaIpa+SwZtOx2D9=C_CBaLhcwFkSo=P912^5c3Y3L+`|IW1a#;X>?&yi!7UM>&u? zjV$RqBPEm}Ovn4nu@Ubb3Daf-QvZ-vPhx@@%CWQmn?Fsd$1ld&SY+d+A45RpLyZec zRx)EMX0>3`gahSX;;8~Wrs0U%&PI3WlQ|riX{MIyvXMkdZ~Wz_Q7%bb02>6|g1(2? z@nDmaop=0jgF_IBsRX90Lg@MAZs$`jCusz;eG7@AacbO7ZNKoxs1uAm!in zPpbBT;cyg>ekoa6Q-tX_CK7=8no*sc=|D@;WW-$tKj-i#sdLssF=+eml^(wAQ*+)( z*3ymA1dhaUFRp}}e%v21Qk=p?;j zSWbxqNK=FG={TVT-m?JkgyG-1M2tQK4v}4nb58DmiDg4;*a|Z==EZ4^Rc7mrB9e-b zw}!P?EVs=FN^M%@40`gNqgOP0iL9W}S^*zOYg_u1m(78uU*B5s-iY~4 zkO^PaiAg+EDJil(NNFW@<0%?b6GW3u5rc079sVB(od@r~L{+c(`B|h&2)hUD2!UR= zGKEJgvByVt(xwj1prK;w<4turr4-w=TTwAnXBBzO_ILn5wbLt=-Wzj(sL$->g|@Af zE&_d2%n%xLhQ&y?Uxw4ayzu6ZKtV?-t-nh}z7~9Yn`@qObAOv#$GC@iAZ*ID_j;S_ zm?1AXpzjtsX?Z*V){)$Sj$UXNv$f!#hyccZ3IWGHc(vYkF&c+z8K{iZBZ|~J5BOi6*yPkwRzj9LNA#b;TP)K&UUTA z8@!#GzuC8K?cL>Hvp8Sshs)IoKRxd9HuRSK!~@p<{&;vll(8?RJKQ!HA0QkbB2oL} zp=1b#W`Jtyrxp%Z++$K{W$I~9cY2LM@tEU_{z&4MF_pEa@Qys@=0fBXeDM5}sy~7h z^KT#{yyd=Bc>;8KoO-jA$(}<>6jcHIcXL3fu3R9jzBdt4gT(j20Y$)ptTJ|Kn%0(( z`YKzHS4hYws@N-3h9fi?e1ZxZ!)gW)>_&9G#q{6Rm>8DX;eaUpdUx;COogr`U!Sn3oW9=KQL>R9*H;kE zKngkYyrl=siTHLo)^gsL3;**w00Vt=$6z#UI?N^Et?k`J5!y7C5*oWK7WNrf+f2Rz zG5YvIC%E$ccD=sOj;9M9pt%}2@=2O<#xR86^W@Rd^1pg(4x+p=Oha~h&Q+?*k@(|aQKwr*88ia zuH9$N8Ays`B}aaC#qfnCd-j;dCqv{DB2*1a<<@NfN$&%nxP#O`CV)M7zmGpdS}%`W zrBeoFLmC@Tid_M%)l#FrbAoSKKU#1vRXZ%&-^De%b`LYIk$eNJwT>Z zqjr{q)KJTc$K}Dk$#gYA81r6zZ_zrq)Btj-H<1cczx8^!%(|bA z)!u1EbA^P3@)_UMC$f|H=_@ zKo!juy?`C#?!bxz=)bcnxtJHX*;QJJBonbEj%f&}^?cdr7;+{1z)HN73Khg%Z%K=@ ziiO*``MbP;oV3sD(H0os7UD4_mj?CZvZ4@=%pjgB5kBC+6@VuVi$qE(sE&|2&}Ne| z6-c(`XozH`k1q65zh~UBF=?~ktD2rPT`Pfok*Zx1vHQybe112tPgb8kk&3W7VUgMM z(J7*Kb(5fY2G2NP-VpZ<{kGPxvV}#!0kqz2R!N$wx1gN{>MKzc+BYi$X9;gCS-81$ za>HQyC=zi|%$)to>lTDz7WZ&jSMc8jyiJ$rKYu}`R&aiUp(-pq(K4oYhLjy}MPK&Yl(@QuCz7q&18RA9yRwX1{VR?2X-jfxYdqsb>NME*~_B#Z3&TSM;hKlEI&;aN}!M z3Y%wnz`teNBZDU3jqx?f_~9ul`zxUQ(|G8NuQ2P=apj7uVTO3b^Spm^X%X^NL%arI zlyxyt(~@Jk(}T|BJZVkQLG(pu`lstvpN$I;N6})7|LJiaN-q)i`JKaO_{J6^qpJX$ zcT@*mN-92hAN!S2KH<%)m z_C|~w-ktGou7=n{J)Hv^$_ve!>!I4`D`fI>S$`Yye|P4cYL}umIeQ&8ug-QLf4{P3p zo-I8V&%wM;k`p+0m&~iLZ~sYo;G^4;jhy^TctjHYw?HKLf3R%TQxG^nf!aSN+miEV z6!R>01YVfPGCeig@(&T@**`rPBDJtT^vCd^!UNPgxFa(2`I+tpZ8!C1*x5bn7G03Z zdekk*?8W|7BiS*A6uYdGMx=F22t4vwYna+UF4|b#Zn41-G5>fy5NvsKGy~$!Li^bN&vpK~H+y<+W=0$=sob_~g*qALIzbbl& zl7VG*gMeKT2j+Um<7!N2(9L>|RQfm>4LkLI64P`wZl}B$Jjd{y<`vrFlc)$%I5NXD zKimU1PQs)8M=lp?j#WI4t38jxRBu)aDjYMcga$n=XgWsq=6=id7dG>`0 z^PU8-5|i+%_=uEN0= z@hB#=JQzQw`Glv!+NnumFzKFJ8B59(-25z>G^$)<_c0FJK`B$rzs6`Tjh1R!kd9Dg z2;b-3Yoz23S~r3L@bc+uEjismLR{5dN*lX*VqO(6q9gx8J;{^!poF>Y1g2tezXeW< z?Yo;YanU;Hzj#gIJD=R=?P__y*=+`;ha@b_49CX;+kvyrN-1hyD+i{EV#O$D&dNcL zOLH+KP*n(1kNlI~XL)d)ygam4ge8NzAZfn>bd4M$*dPah*1K-4Hk~^c46f%k6*^k= ztxHCftP=g%iYc{b&g9EYtJ;fnUt);`&Z8y)9*Z$|k4Op)ZteKDYZ?Pp{_IdNtrJ!C zl^%G~U3d0wv%DyR4f1%-P8)1mu44%sUowJ;jz$|gQt4Q}belE1i0+eqQc%X6p(pc2 zG8Ar;UQ;%p0##q}({caU-HakgZ!o^r?Dr5JyTRKwVq0$iI|crYbjy%>4S3t7HL6-hwU9)YBC2s!z3kX6FPoo z^5&4PCh1CpIYrV$JALf2&^U?NwCp^y(*9VN#NWnxY;|pk$p+9bP_YRANS~OKm2RdlQ_SxuH7B%r*rj zyE^MtJDop_*7Gqi>MGhC0-q|}uw9K{5uQu|1q)sCI5RD!R3%}PCT8wm(G8`oU5Z0( zcIo2%4Mz*U0y8EK9oSb3*LaujIL?~uVoU~CeY!BrhYndW*(m6sVh^j4P_TIWF{XNFF&Ltf?Q!u3x2A13+FY-UGR%{u^&-;% z=QCTdB-gb@V>;R7NAd_Y-svta5(!Njygoj&XV0Eq^?+kH9Q>T8U9_4dl=hHHn9Sul zkb6*&`pMv^o%o>h`eIjEp^12uy5!8b(HCBi`XnSa5%qmO9X*n24;M6bmUp5Zy_Gy# z@VUVNPBh74S*X?dTBxKx!UxedrCHHxsF00L z!-G4M%3xLQ6366(JPqqe#)9_l;Fi2eLB!2LhE5<|3RP05g_>}_Tz~{wOI^j#yg>nZ z!hnuzW3E&?{D>br88tg#<{mz#fMAQH@e30U2VqFka!2i(nta|~hX`fWqUt*E{cH&{ zp-&h6AJA%Tdw>nINC;k#8oNKXCN#KlG310VjDPpF0nv5C9if1g~PfDKN$eZ64N3dc1?efU+VrF zXx4-ocX8=J^sd~SMY&Z2D!`mFrfcSAO+Xu)o(yN<;zLIiwY1|IpR0zkTC7flT0|> zIb1RE>yM?~3pZs-T-t_SY#P31J-nGNR@&Dcv5%0kjWC+qJ$9MbdZ( z6#3LVrb4P2EUOYYOP2a&kEDu@Yk$-3*%*v-`soNAOQ)hwONm3tISLC=~6LrHVgExwiM_()f#_+YiFQEXOY9cXB?j+nHbg-9%C5TSQLp|ygp{IMx$ z7fd=+dUVI75+xL_Trv*37EqkPp^xiJn&ytjDh!YeR&A^UL|o$It7NL^9S>asB#xL{ zVv<^VduE;I8Jbob?d!?ZXK~p^rCZmjW3(0&xw#>$n&qzCtH;RL#PB^Wb}EV~-Kr%Y z+$$Vl=<<3EmfS1HXp3$Q&=o?Z80G2(V(av|@$~{H2{u~-RIxETmDthHzP`5IY!Y5i z{xsRfXgaq5(x|LFK$!z!!Y`q-X6zXkN{o9*y}s z$tec?@3ebu;7NuA91fId6#S@Ngtj!lE)*fp`2tB|OulBRE8VanWc3z^{U$yW*Q;Hj zD~uo?8Y)K&E>M>3kg;o*+5bkGd?n&YE;9vIete&zutHD;E3IuN(84YD0e7}o1@4GF zPy+!)^?SY$tcT4u)f;=Is!a$|d(DHm5SiX}Dt+nRO^Wa<1f>iCLM^7ipAT#Ly4|kG zC)APPUYXQ7m%O?syt;Yh*gsg*-;J{m=a$PmH=TZ*^`>HZZ0P@f6I#Ze&h(|*hO;D- zL!bV!`48$z71l-AGVqVuyjk-En(FZpJKzt*xfNGE5c3`bP7~(;(ke8caiN<6Y+nCb z+tcBFFm(HyP8MCa)jvVn8Ff$XIlnSTyR!1@l#?ToMks@HwECv4jO196pwean=ofP` zrp~4#WX^jZmcwY+11i|Vt*dKrQI>sGx53wg@hm!^qIONvg#t3X=rVU%P865LHC+I4 zIGdQ4b6>P<-9-K0snw#3w>WFz-n7gLE!BG{LW#AmJeh-hNVHVfIG-zE3Mg&4vI;sR z)-O1pfsS|@_sqNzrp

lk+fa!(4YcO()o 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

yI226N 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 delta 251899 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≺VYk@m{irt5tAk=SDYU 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

|gK*4-}@w~Krdx5`r0YBBw zQYR~G=@?akob$U-RO%daKU~iF1=X%DeBm#p*)FMem%5AR?N-YiEa0GE{)ulx6B?EKjd8 zKwiUTEKj@eCT_r6coJ{pY5anxeo5+Iara&BzQ@^b@IF4khxiCzv6LOe?+L0O%Zd0z zCh@sZg+Ew9c8={^xz0)yU8WIye?ZTB#4p#&4Gf==GD(UF`33OF334MxP6pD~%*=lg zOs~_(^Cr2Or+2Z;-NH4EvMi8WInu2#tAx=<)m#=(UM_@rXa^ad2T?5gLDtlRWL*3e z#*I4ww`z-bQ+Yd^ofPD2tLaq;sruRBY-0R-F(WOvrRA|&ENQuc7yor4XVYFmz~lRi`aoQQ&hC?#n0k;L5(9{u>ihlW_Lk-f5y zUoW|Rj(T~O0qT=pa>9%WY zA9lBP*X_33-R@Sq+HKd>wauP$?@TfiCNVAkeqp}X`ObIVkIQ%dbnGPn=g|=rHbQ+z zEYc7O_VvbN-SJ=~(a;f#?lj^_BY&P~2z5rn4V%KDuCBA2c&b7nOx_;c8Egm#qumWH z?c0rxB*EPk3LA+H!S=90sAx6;Qpr%b;gVpYCn?*F1XpXQI~q);;tbPR2xdw12V3DH|!|YP=5@KP}&@d z8f#O0C!QhF(;VvvhTDSikgPMCO0p-EAk1l=xI~y5dps2-)YTSbyDn$9jb#;!^No&z zuoGneWQ+waE^Sod%c?2|)Qu{XI#EM#^a&4cOvULcP9t~=P^4iR&R_<+4c76@+UC)U z&(e~+rQ*yy6R`+#;w*x*J%8A-Lo7vJEp>rVtHFUH74?MEM$5d9<=nb#)ACd(+-Wf3 zbtsjexf)YCJ#vm(wsz^1f=Sx+U5_`J>#`#=OS6@Sru|Jt2M8W9b>xW=-f zb@5m)$Ew|fiVtrE% zy-_3Cz&niUdfcGm)7fwmWk$pi8rB7qJqZaXH;$Ng_EHu{E-ZV^9>Q7q2f-v0+@4Ar zrf`;JS8m*lTYpvDa?+y$#gE>G+c{-)u>6x5^ITtB=*i8^6MAkedhZ}iJ;{DrQpp?x zx|8i9SHHQZZtTO|Vm$W{%4+k?K&)mj?v>N~2>QrrE&>zk03J|rKfC)$l&p*<3B%PVSIs+M1skV z9wRY^tFhaDkz+^p)|WIqiX&{H;b0=!6zw$nTDr!#*(C5^)^HSG;i4gtYEPI462qFB z3XrSft69g)*&fat)A4l;PvA+WcxN!2GUVNl%Voo!@wj;P<9J%bGkBJLA=uk%L^IGtUVD6vwevnQdIeJ`5Oi{O@C}EUeoYBe4m3e+aBTk0+v6Tu)fxa)S}+wL<1O1q^;n7k|jcEtU&8G%6yM8J3tc!fxWz# zxkdVRwo5l@9M!0V-DKp!6&y!6Cq<}Kqsdgp(q{b4vF>goJ^}AEbLI>SywOEos!*vs z8^2EmnbB$waZ3!w$w=p>yLureNx#ivD#csCOE*6bWqtjUc>=A^f+K+i`T;=5Yuamf-rv+|0lg?6UMqW@@+1Fw8CPT5P zMl&TUxx2-H6NzBAp;B!YR4SU5VNjL0cK5~N-KM`Kf_+l6=bX+V%#&+<=6P>0o)F($ zXO1gk(XLQ;ifc^<9lpcn%V6xxJb&SElnN)MTPx5-xy%m6GEdLR0$}=MW_q+B)OITv z6Q&icO-EV{Y3Vv+9o&+P-$qE}<-UN;X53^&jaR73RnBqL%Y4KnLZ5yzcwXVa3LMK9rTRAu1v>`_R){50cHca@Mt@k8O{OC{zyANwb@BgrX)!Jr83(J9d_*{DuW>*8)a3@|hvuav8WN zoQVSZm>$oiM1K3fyEpp@VCKZSYeK^)WiA=l2;|1X0&4#FTP3YY?wZ#GdrbmWx{U35 zm|c#53#gszLSl2hY(gP=LVsbL!-TjDr_uz!;v^11_676K=6;q!8f_=;N;J-H!!t5XW$#J*xd8Zgz@#xV(}=axq=6(WmJKu1>P8I$^3GhXXblu+K*EHq0eb_3GpM1g;g{h z5XX)Q@=;DTvL8^2On<&P3J~|e4s*fLJeGZzFumYAxwJ`iZ;hqm9hr``zobOk4^?58M_?<^l zraPa<#e|o!c+LPWmDO6pp`qW@d)8Zs{jO)Q;UK(&Xbb4Ber)o)b$1$@$?qnA@gS}W z3}Tyk(%z2_zkeoha|NtKFG-`zKS`f-0%fx6**<`)!GpH#=E81mJ)FiP zX*}jPDIUb*foCvF_gc&h;A{Q(hJZYk#y5qQa`IP@zkkwvp*)TLejM{xl__|^>GvH) ziC(4q>=k+%gSw{s(l{Piq#qu7Td#T*i}k8B{^lqa_$y@TG?#kCaet*=aRP3=(qAF+ ztJ&m8I@ykP~)+_ZgVX7>Rf7dmcX=(iWsX-hF=-z((hrdd% zn(R#DKlL(U{I}%ykzW^67?%OI5`VQN-Wm~anO}>%5D;IKr zo07K-cw3g1_B;bF z%YPW~@5Gc%3G2fW&RX&N`Bwb?$6K*HwV6$Vlz`uJ;-i!p%VN`51XXE{?UWw(Pu5EX z)ntFEpeofT3o0H2Rq1iDuEnNx$yN7=vFYyP{$kxNSBw2_xylo{%7a{WA2%bDJ}NS0 zbT-@bR+^j}k8dgKPM8ls z{+sAAdW_@n3EMli4|#mV?y|c$%pbPDWdAOY_t@XGzX>RHN{h0E#~NjavV-px*&@nO zhTo$ck`ceR?NZX_IxQoP@cI|#`VARzpY0~)J#+n@jF@S=$+3uL+4eXV%9vT#Wq;On znf2_nu&HN&vAq06=v-L@U* zu|-%8yRhGO6VLZxyX`LQupL0y_8=m*6NuWrhhEzcaEkE505(*jQIQyfinHqY#6 za458p&S5Q{r+h@`(jqPfmVYXLp!4W_zP(y`n--Iw3xajZFR781uzq(cFS7g>aItV3 z9CQIK<)Y$j@Q>M{!o=u`6`xCT+E=C!-?Dyi2v=SbgXKSHV zjKxbEY<0An;mTQmelFaaP{mrTrc3EE_*j>2YNj>N*~09Z*=zI^t$$_eDr{S68nrNQ zm8`FKX&qy(Vr^YWm(zMwv(8R8vDC2UEG$!Ozk<`6vE~}6mEZNT?k3X)p6T|zRAkcP zW!+h{RI&ywTB=x&78ajIj7!}{CI`m?mLWmp?q8K)%+=ddg`a|F4HqrtOi z9AZavshFywO2u^ZuTI4R6^m6|sA45h@)7EJe~5jvbR0l)=Y;upm5Mh0qjAwK2t`Dv=AEDi^wWj4fo8;<)?fh}v@bn!{!r z&O6WqQfB4}qYn|`HquFaA~Xp|hNQ9z>(IF9c=ycO*%Wt{R0j9*rWI~DKlsovq(@$s zLll6kKENH$L^yzNS?mi8!Yh?Fr80S6^dU?!bcAN+~sqW<`Z=9(R!v$fr>@mQAuG#)q$&ttSf;EHN>aQ9NeU;6Q zFNSh^{SI9l-PGi|+WWi3su1m^zBhuRMf0plqh+sClTr`@^I^Pwo*U+xhn{F{u1G-p zD~u)}&Sm*o9qg99>F@$?H@A;nZ~CgA(jcwj_$;sc% ziTyNRRIeK?KU?pS$fb~lzxfeqkQ?weEbp4u)7Pk-7;ee_doDHd2;ggT zIv@4;1NmRui40A7{ek_~_cz;ThWK_qnN|{zfkn!O14)W`d zi%GIqn6MGiD;iSF2g2ENOa~x|p}HU`KgzQmeb?VvExC zrYA8N4Vn_E=lHnVY+PvUJqVVwPEYHST++6(-5UlS4AZ5IhL`>ZdtCJ^M47mk;wzdu zOSA1PXA7|7G^YEChL$AFdqPsAmBYJFDNr8B#fotP$Ov+>m$PF>a2R9V*=r))=`R#S zq5iDxE6~k%s=E6KXJxFxJfRlojaOJn@?~8snDiD(%BAtD1bY<>Zb{MM#Fd-~hgY0y zT-GtoJ9Jv}$+KC3sIKo-gw{LJm@5ZRFk|DpcaU#IWRIw;$0c8uP`joHtA=ay*vdsA zlv*nRFhCkC%qwbfsPnzm7?Y6}6EL{P$(sx5?p3fK?P5o+{#?&AyF0si+OHf%NO7&; zC`c4%U${=Ihr1;OSI}n&`{QwS3uKs7;Os^;n@T}ySrcuNNqC~|O9umWiXtfF8*eY%S`UaE>GezGTxie&Dk>4n_9;IPexa)nr zpeSo_n5XmF<-Yr876;0oteHbI|0*?yD-^szWKSL8_skT4HzEBFFe;g-m{W2z1BHNL zqHj1=(}Alr*+KDPU975LHZ7o15osFATTG{c7!|o|g4fhtl9LT=j@j30?Y6^TeBI9g z9FP^znkgozMu=-@H)YHO5US2c*`s3+2hU82dPzyOm#0)5 zTgHf6E^?yDsM5T86NLyhs1a*K|xO=KpzGTTn!gXm{Z5$ah#-aD11JJ z&pJj{xr(;{lchs3OYa^=vMQc1mUP@0smE?ABvM^YV-l{+P!?98I?APRR^iIkU~Jtc zt36w!8=t$fx$n+PUDmc-uz6|==yMV{t|(F`+l@6hNpgU!VOmn(O^|c-NPl#L*4+wk z7}m$wmfsZ;8N_&4jBjn>wXgK#rrLVZbzL``s5pOr`MSS|P}n zC~`EGoELKU?JIzMXuY>VS79hl1yWbfqYralT~5=1D@D5&=SS|8HQvbsII_yik?|&z ztL*2Ds6Wioz~MI00%pZfWAe>KAqavI&GRJbFJUNEoeLQ>$q$_Mn+kCihL;9#O7@7U zwa}O$`1@V8m~Vu4!MAVQfbtDdJOr;=PO0A+s+wAKh4ati^POa7IHjU}PO*b?EHYSD z_@bT{1hyKTg6l@{5{2FX6#@u0vm~XGo2ibcP?0|znUA1k(P}I~yE9CD7cv{=*H~iM zwvDBx85(LwdvD~&KzenHYnGrxiIL|x<$SKO+(%A$8F6q&dvwS(bQb#>z;}}aqXvl^ zjP&p5UpNL4Lnyzh(w~{DMCgC2i+r?~Na5ghN3P`8>G`+yulAb&ei|(R31sldaCx*KIr62Vz${3;BaObe#6BGf@oPM38b~C6>{$Ozodh-ouY<9R$y_ zv8#iI6<5zZ`(3Y(QQ^Rjbx--1HO$lIRCRQk{E>yiseIS~qSu3biE1(HAQ3jrow{&59pag|Z zK?{-p*~FFUj8B7_N_PJ|)M(xmRd3`nG*L9K2&7IDaqa1yvQ2C$AA_PmoD>%@yGpUa znaPK*&6f=~s`|A3%NV;RIRkuHWk2S*z>}xl>lU2j@whxJGAyiL-H3J=v&{=aKAF(d z5n@$aZx$ZVkeY2^Q@ToN#E(zbgRXUyj-jn~{+B{v#pqwL6#Mqt{K_?=IxZ4kH2ArT9Nab<6t{2Gdu2CORcovFJ;>7_tS2Kiy$;FefiCCX;V!;j*y1E1p zP@9Uy-HMiJz+E+SQ)fQc_@s$L%jV^nrq!zm_k79dk^%a2svr^XVZc6adY^u;p&)@V zOd8?-q_#e2Kla!aW<5?^d>|^f>KogImLMfeBqxAKk+D2yb`S1M>N;{jopjgqF6O~P zAQu8qomW#@)1gU=1zYw*o^q+q(*d_kEmQB$QCl+8!(F~uJgYG>JuH#W#NohSB;qTx z{yU|O(p5=vaW&)A&4)Db3olD}(N_yot{m9-$+~zc)RUPo5oA$K(~m5n>0-W0^9~oC zcwb9Yv10oOivg$8XhD{nA@#%-WSW8bTD%XSrVgwIX)YX`z*Q0*a8kD?<=i^9&Xsl< zvN9YV$=n`ZUX?FVpZI`lGh=<4(8)Z2sp|_3fY~{PD>V+BZ$QiOt^Tx*Vno%k{cBha zVHit9h=bs;Lz2(|ZVf(A+wf}B1CbUPs%qWt=Y2F7aFiSm2bejSeZVB!cdebK?6?D{ z=PNPCP8$x16tg+*Giv+t?*@R;?y3w(bCJjrOgz-yW^uTf-N#@6LLEUeqxvwceVVt| zQ?k4FdWBpKjF8_rb$z35-&nkatK`W|h%hGy&z_tC)5s@TX1A;N?e@QqK)PdYuQ1pj z@CkpqW&vUQo2TYUH!2tr)uKtg#&-bzXv^QUTg&F7n~3|Y+e-LiB)hhEe$EK2*<-)3 z|5(ZfGokwnu@IlC(*<6KhfI-?P8+8`K;fLly0+A-GAui>M1M5cc8~9Bp!y7{Q5;V1 z>!eD7SUUJef57`^l|LT}98z~#^d4$=MW<$d;@?K6-xlcUxbz{S^ej$!4^$ae|WDvK_<_>25{8>-z;KT%x%2=%TBX z^`J{y0{|?q^^Cn#jHai0Qt#M;|Dc_cQYfrNbpC;cA>p7z9xM4_zU94(vu4T-R#civuA4?nygT1dGwOA1T`#CK1nb@+M@l9GzN|UYs3gv z(OT9NTFC2*5!?sfV~o=Yx>TXaT=TgW``(_0!Lh%-2JEdQRO|x(+CK}1YT#z$+D$}; zY@Ih=kjd-zaLp;E&H^y=6CANeSBcg;MzPhRim^KPIgWasJ&FO+DRy{{sq)0ngkM(v zgg{1DQtVzf+ZQtX2dYhX*siw}e^{(W_x)~kP>88gB%I|mLj{1xbNyG*BD76e7;ADcDm- zfIxGc!lcqeo^vyJhob-Wj$H?0NSR+8kk_3pOpOu%&)qI z*-0PYx42;f&(oB_&YwiS$crsSVIWYkWW6j`1hy<4W5H%=rbrwuUzJ)S>ddC8wsi5# zLPx~IXhgp$**(uU=npk$4rm1VBb8rR4H{q`)D_)^hzsC~xMBe6q8Ri`P&OA-;C!!o zc5sYm#Zb&J;0J$=QD$`ka~?DQ%w{RV525g4?xcuO_%B}@ap&MuD82*L<*oT;e2TlN z@P;N4J?$aI*ei_&dx9;{=rTNy^cQU3Vlr@s$c#vwa5Y4>jWE*nlWy_Noji zMAN1GXmZb)nGOPW?v8LgLKX0tF-aRPR0l-4?Fay{_C1w(86#$J9PJumjDfPPI6^~! zkKn`&U3KO7ToS#|&P$ND(2E}-#y_Z@b&!ZAqkI{Bn=_W(JwK!<)l!GElyA5D?G`%? zauy``%;*Z{oF~j7nk_R%K7>uFwi#%^6_8eVL%+3l=xMl3wlwj(GK1SxDdD1G-n%Ez zM=9Xn2VDTX{>x=5A^ZH)Yi$BLeGz>~Z$e~|iI%nygr%XehH=UpxN*oPJc$EH94?py z2I~vrin&Z$7@X)%6kQ3^j(;mLCkOX zPzW%;Xl)tR%$y^H>T<%wG43sHa3^oQWfp)!6@=nUWTz7fQl92vYVum5_V3hUUNi|( zxr)SmopT3EB99fp`22mr%eFF8-b@ha>-U}yzbHWDn34Ci zTELrkGhPb1V@bJnTsbCSzf*MJP_e?X^D!NG@g??#v7xVOdaf5#qAvs;q$VIAW*6WO z-TES~BAqw%(vR>?McbQOZjWgz>>=dwTpt`vfS~iGx1iA&#oxaW&?%slVN9#kkt#sl zd~dL~Oe(L+4w|zqOuM%(c^!cM_^2Y@hgx?n3@aY?h2;;%u^w2q1_`?bYuFpIkk8i$ z5kR^DWosYg=WSFuEyaL2SdvOaCkP0I^Py%LEw$#>m#|#K5PEa&h_4Bl6$oC}B(~L$ zv3t^f#{qd|IO@V5Gl&EA{eh4gF2p1aF@DODEPkdKCk&JK=7scR{Njdu{dKF4ajSZU zeymIR49Id2o(88UH~_MNcx;R53C)P;?({^NK)LJcwC3JZ9}lxo?M zGywk|L%K!$!E}TmDE+1ql?V}O{z*#~6^(d3AiNNM|4riUcQ2-d$Of+RNu9;my6!@cY}HC{H9Dxwc^UE!ZI zuaUHWuCLeFRI@eZbcNthH3_(-b1)WTn02V!9?pg1p>_2hNS-mL-@kOA&MXLT%;*a*|TcHg?(bkYUj+X8OH?mKOzK)NFp zkB4CtHI28b86u~Bao){plemXUL3v|@22dOz+%v`oWdoH!sc6QR!SG9yxC4Y2_ppNy zKL08)Z3cw?-l2Q}x-Ydo@<%3{w=<=#uml`^2#OT4Z(5y|^4mvSN2gwDCzb_NWvj)(U7R$2`7$(wSLEKD zIQ0Qf4n7~Up#@>na z)%mlpyBW*4%W{^3DI4~OO@0|pt22RxUOKE;ST>OurIbum*14E)kb5CY9HT4$ z87+~vdil#&R|60wu+tLNdws&o>gy5a^ME>o#ccd>3;il^o0|*eiI45CvW92K zvVr~K`Eu0+Xi`326ZpP9t$dMcQ65|`AG{2PL!*ODJ_fkNdpcu4PI7Az8{tk@Bxd<) z)4jTkkKdHxQI3ye-c}y5IWTd*JueIdD3X_Q_d8o>RA5u>;#WZ>!;;Yi9fL&N3xx`S zgbEe-dh|_QeMk4XxZ2mmvzmDt68_}H0d!oivx8kGL61ZK@i)z#P>-bYYUh$0n6CT< zdyh_1KLD?({U`_>!Vqd^$wwrE6FP-zB%r_D)w~-Mlq*OEX%md4&9vIboMac(rUQI9 zwSsl0_Mkh4;mUqvZxsX(4z?rx#Ds7;X%{T{xRru=)!aoVbL$J+zP{KCyX*!Eg2Sz& zUak6|r*N1lT0>-Y2G<0rKD&;8)QB*kud8m;`vc%;cfac6VCkU9;4nvVrNr^rjxODt z9aP(jPa!+U4J@vG8U8!6(-j1x%eX<d#{->s3 zZ}17#k1iz~L43bXd#iw2bNKiqs~31M`9s3icy?R+U!Dzo%>1m&wS%>wZ(FxogMPv@ zKOFj?JrQ>furTCsf6|yfkO*qk8Ybro`RXTq)nDT7mZfzW2t$ z>>Z~qVMm_p!d{-7!d867gi5wcOk)S zfhEnkfSwF#LBwN-l}E=0Q9!brA7|&I<@%gq-sqF&`eD}49~T^-?8WtyD%0M^h*54# zr(zfAI1htq)U4Gzv6C1)r=4j)Qr7Ol_>5z)Sdepo5FW8+TS>y}va3L{PouocPL)+P z#IF7bbK=kBsmY1SG_dW(?GK*(Y8DZW;#oLthsgqO>>*pKMLM8#^GW35&I2dm@I;#L|G zH3;5WO|daZTg!-eLXKIK{9mz+E@D=Am;9MK!4T$kZITap8VI_b?6S1vuo^x3XPO|o zmQRgFcUAty@8?O9Mt_*pk%P@;xr1<4WXXo1$ST$N|4;XZjn0C)* zr;W}hPP_^=}=D+;Ht?3W(^iS@7NcaqKGsq_yCndP@_|H5qCblVDxy`L8jN#vzV zvaBR6HJv4z=m*r6(y((=qa}HDY|^KO?xl3eurKQV9#NX;-<3#_{&fx2Eh5E^Z52yQ zkKGdMvi6o#t-`(#7DTqTj&lv5EJ=vrWx0gYn3Zjv!>bI?VKl|0_Oeq6C;@pqm#N&-=XQyE{e3ZiaXBz7ltt2>MI2ppV0UQzBy zoNqZ79gJ8hx>V)msq_E_JR&C@o=ioxxP~O@|GJ%DI_c=hMsIde^dnsQ(9$xzDD+z> zNnl@AG-q4OQ;rWUb{0%g4ULmvMStl}lul6#T^vA5lStvbwz9UiXz281D@m2t!HZ}a z$v7ge6+TSos}zfeK$rRfrg$s*5uzZRl8aX8pv*9n;tV;FZ6R9(Y$wT0Bslc?;+Z8H z3X}--W$$}pbzIo7TV<3q$y`q6VZU0zP!8JHV+11%TWd(7_`@t1Q{XGa#ETuh);Qrt z=M~k05?mE(%Y9r1#(Y>)Od~h)fOMtaM6h^VV|NQFu+J9hvG}=?E#~RJMdtop6j{G2 z!^gT2KeI}Lp)%9~jHV?^zWF!#+b-0bytmUH&YK<0NwsmKyeOwxuf>Mzk-GzKl~p?m z?5CZ3$l0_ArYXd&u#?-={-u@=C zZfVhRchi$}%lV|f$nWMT(T-j+QA=dYeH|t|uSJe6X*gj5(g_hc=~s!XntWT)Bp8{X z`t3!w%^v@dppX^R$4Z+YjzrVb#;pBvQdv)Uumqz_PN7KLkF{9Slr&Spj;^$G?l$BC z`p_@P;iEtGayLpUtYx!CuMPwIRmc7NpP6&rU>i&RH*WSAo0DYwM#Wfh(wQ1zHpu*= z0R@6j29}s8AUDSgLd3$bG-q+IbkRCS+H&Cr*)fUPmJ_Ph_i?5J{p5`>X{N+wPn&kY zUO!=d(x0D?D@rjh6e2rr?mG23l;SIpAqX>!mzNa3uEZ#Jr3qHp%Gp*7FE!1TT0blx z=XVa$d_+Ni9&Md$cJ9i&x*{qbGs|-_IaVhsppAul0)HUzq6#7y0fGCMcQqEh0?KB*!WCW>*H&NjIf&p&7 zEIp<%fY$kp+q=IA^Nc*punO4kLDNHrgMBs+pt;wA1NS?s*Ji;M#Y|4gCXx)ryDyJ3 zFQmg}-zX+-XGc6f6EQZlEqAA&H}lb9r5{*7ZzmD~op3@9RUAW^ock(W4x#b<)6W}xggYY_0R7?KTJ}^D)|E}Q+ zI066{vn1whU?>o&eusZw6yErpJT6C=6>S$0o=TD#07w3FJOc;!o_BfgL)m)UBYcT% z`aQDJ`C_U{ut{i$ZOFZcdJ_eoxtz@8M4k512CZD`1Y>`5@&8ke%GwV?5EkojwqKgW z#cg+++HxQ#bzcUn0b!=$OaRPlw{>m+pDUhr#o*AGSXrR`aStvGCvR(VhxGi8?-eCd zmMG98N51tS%&X~j6hw}Mm?fnkU~E(FCGTxer$iWz^DW6xwTW`s>>Ih((R8}Sqk%Cf z2U{+WxHWZj8w;UWA>}hTL;tcpu7X0|hnmxn;>bES5*3u|L5G$pEY#74k!h;{8>duu z_VJXjJ_aT3RKJ*XZ@hfj^o+~V50I28@)fW$aE;x@zzkJxZNmaC6ga~og zliZelnlyD#8%%oncc^{Tce==sPN$qo|8(}3S$AWwvA+^|_Q0#=V+AWO<#lL}F3ROF zL^K+5Ak1TL?};M4mqOP?v&rrV>776%As!BbE3eJ^XVV}|| zW)Ifac=nH^+8p0vM+Cir7vKqv@Aa~8Hc0RO{N~p$o8o+v8gEKuE^Qg0KSW+@>?Lhj z$Ui#^KteHjd5;e8mPlexsyW8+6D7t5S{D*Wbr33ehv;SV!*5 zO2K8lKSo{I91xar<2`;ltO}&(89Gkuh0ZbG2X=Emta=o(6B4Qnz&e({!=wD9)NB?1 z8>#){`ay`EpCEzjQgoGo0tLDUh zpgVl@P5DBLscG7)0(z#7Cf{J%Eide%C`u)UqF%0Gb?C1fc@Etu497LcBX~1ND~ot8 z9wPIE{~DOyH~VH^E;q9olYRjo!^8Vw`uhftbIN2N+ef)Jk?9s+SR^?nY&!>HCc&@7 zE&wW(dhAPWmWW4u!^b^jviEf{^w|ZsK-y=II^}yLXRaG-0a_soeiXHsMn@wH?!kls z5R$QY5gT!;RD{^*)A2peQiq_yoW|8vOrESLKKd*K*HU1eM&QNsf2GYK1mRF=X+sl5 zA{3G-k|uZpT}kfH6B@h$*>0*Dj<Q5NdDVLoK28@(&na=#7w>9$hY=> z+)bNW8>7SM3sLyDb{5+a$I6~2v0y!}nke?cf&tlP0Q3Afxo=2-9lmQtw$k&fGtGx9 z0Mq7&?Txj(+9*Prw!Nk>l;zejsdta@7=xdd_~k0!FI!Q{7yeGlb=?ImVcZa+P=VIj zm7gA`6{TbDD{g%E0no1_%eeBqX-aKol+aIg4O1%qClvV2$NoNPTrkQArmC`eN*?S7 zB$q(r3#YbTPf5cA~* zm_=aUJXWV-B70COLp+kDLZ`35pppZ$T zmGV0br9<^Rsc6SHj)QJlzrRF_4`s+wNz%;TaKDAQ5@p)lnXQbr)xr6FsW@zduwf~@ zCc%tom8`g)OAaAQEDW{DshVk5wvG4|psr0lXTk*?m6F4wwBQ3Yda~4f-s$=VmOM*M zPhTxd=a{5O^wwYeu84miMHU+q8U}hi=R%&Dd+D>MK^4tf8~M)kWpuTA*frg-YJy-L&=RT-#bQd*r2dJKrA`f zdr;IMv18D(7ycvLwD;?oH&yOyrqK6`p>8*H3GwDzOH*t(ZwIV7?`sY72= zSuNK=v4)Cg&1SZOD#NB(Mvey)E~ESzY?YIT!R{o#3l>_V*y>F>|^J%$E`V4W#EuExVuu5Rc5HvV<}L zePmbkTyV&#BEq8)@>!FyqIP>_jjb^~>MrtUFb z(S^1htSVcvF~I_=i`16c#h zxvYYQsBX|J>r&3IsK9bV9DOu$$nee>Gl1y z&TLtVzq(j})@}GWFY(@XntT6>NpNA!%tIUXon63)>AtuYU;A72aa!VCLC%Wbp4I~9 zm80HS)tniZrIKY&w%)MyT3m`t_NfgGbM40exfAOe((;I!4KkJkxJCdu7Ar_}h2&~w z9xEfH);mKu$(jKlAT~PpukWo&rJ!0KRkCOkoh24NSMExcEgs`aFkMt>&9RN(jkti! z^~AR!@^GfjlW0&9`p83H;Bjj8{u^V_(iQGx-h@D;_xS5n3t=85N5(iuSk{Yyy%5As zq#5%stTncwNVPAy_!|XJY^OE%uA==XRP#e_d9Yy#)|@^i03bj55QhetCruTcXoKZe z4tiTt+?Jt0yKLMfGhMQ5!=0VSb1K#xq@Zj$SAOoxqpvS)S%zyB(-AeJhI`7@96F-` zD0Nwqbj^9E%&zUpgI#KM&3_MMP~ms2pOgo<9xsyDAgxX4f#V4!tGPydr@V+viqoHC6GZ>P{p5$?_D0_6SjzQ;LKOFbMVV*X z!R`OhHfvWQK-li83ep?-AmZ{b3cH0Oh#-20UcRLQJJ&{b37Qv8?qAEopm4>Tvh|(-WfFo2)838HflT$V$;rZ#`tPp=YuoM_7T8Mt924sjJHVUN znsg0d2288W$r?yAzzHvlVFRqgY8K#t>L!S611zT(bLvq|e(~WA9Sa^A$Cm0+n2|zy zsZ5zNn!W9@UTxXKruyHJ^am20Pn9rO*5}j+li?hxvz^v?E!B*e5M8XSf62N|p|Ac> zn@^RIZAKdzcgvq1Mnh)}8XKnn;Lo=W3D7845TCYa{{L^$4H z<<6|l;w;Os(@nVfC8ghKtBX#~=-mQxUCT?FwZlm`oljm!3UtoKTA=UbN={zUtNFrYIxm}D=KJyZOU|uRw3R>>kja1um$;5C$-yd=^fNGxP1iyJ`^O^(Kj3g z1GoglE=Yxb0CPrUCHs%mKtqE0TeCpO0>EIetr`m5!+-w0OWI}PF(_ewohzSR+WDhx zj^PSHn*+A<(B>k>3&r_?8mrz=v))k0ip77gZ(7%(-vUNH6Ik-2Vo8QatxK@E%6k7a z%VOfc_35EsczH#}DgIgo#F9^}LLu&&rBJ&0(aPrd>I@&nB17+|z zqW;A06TGE=SF!|bY{M#bf-7A>k@ok~25eja*%(47eKY_!J9-2FHa2mUI(>j`UKDOQ z(j+E%33^ibD(s3r^(cz;giPpng)e>rNqWM4zA*nhgT~|$QcDREzyHA1b>MsMV+PC+ zv@eTT7zIdS0u^PVT;`*$%a#J6>P?LHEe4zSplOJm++ zwPTOL8sRA0a69YSw;swkGED>kf4HKxddxEjTFJ8h!RM~J%2YG1xJAu+^VDHwIjcqr z_$G6jC|&j(U_k`#IG2<*24xbC8#MD@M10^Y?oeu9(-NNRyS=By939U`9Jp`SMj0nF zRHgt;;la3ow}tpwbyHUt>po4B=u}(~O(IE)TOCvB$XbLqP=6`<;Z?{_&(nm9OlBg5 z)6@F~#nL0%v-|{{Z~4H<7v6hF>WMq6E|63D$!7?E z0B(cDsJ85z%oC4Ujzm}u#JL3R8_ffMvupYZAQ8~m?uVxtmR*+JDk4>vahO?}J)i@(N)H4)Qle-hXJ3nXao7_e9)-nZkWXAZYRj@Nsu)QKStp_KI$@ z_pc+>>NP&w{k6oj-|N*GCS@aZ?zG_pY(HA4M~p(4&q|l40mBDeX1;of!(J(b)Ng_q zbk;1^?So%sLUE%=8Njq}vxikulxD_)Orw}ah^{cMhGB#%^)aG3gSKm<*$llI!lVPY za_v^yS{J!?VNAe6+;iM#l1s?PzUIxz9S&UJUC(7ViYX)-d&WQ}EwWC!{z|w5%(#@n z?X<%^FNk<;$$;Y(lbDqYci{oG!>_C>5Xa8DUWYW}EB<45`VT@My^!7eV2XQzH{Z$d zL}<=y2Uua=LXX{1F2wkvY!6eCj^={M@}jfvYBzzq816L!FV6}q38V^ymiEw@|`zWP4EDwiZ4i7bJ z97n)-G`HA-Hs8biixuQ{MY+ARPdgcnHQiVI$a8H+9`5h78t?shdhp3hILR}JN{SzZ zuF$Z5LM?K_65FZjL||L77A3q^Lzog=M1I|nfH@YOk2CwN&2hK+Z2m^XRT~mh^K+BRj2+Qnov#Au#>+lE#OaGF+g5HHUzoShf(Z-!)as~lA*V0K`VaB z`L6F1daOufOy@Lvh5z1_WxEJsBHU(anyU!hNwYY6;VE~})}^y5s?fOo4P z^C%YD^f{^*7|yNzbvM_f-%I-BtYIxUL4CEVJ-n>L9b;z66$bg8a!{KU`O!S#gC7nR z0em0BwJ6WEIL#K|n9p(@marw4^vaPj(7I^s3n`P!YmK}sPVdVJa69+_>tO^{*hg4- zlaGd9GS?8)sR`~aFewsX*1*(bG=v0jR@}wc*z3Du#k5`=h_fl-)~S0($k5}ccxykz z{rU?CexyMm5WY6F3h)!=EP_2!r>jk{urro_((djq-w4!g?E!W1xPZK&|={f^n+_H+ExTWFpMum)|V)GjaSY#nWSl z_Xt)-VbVefP?Lv;Vb>5U;AD(^?bSzNr>|M0w)4bK2_EcuI7glGgC&^stKOVRRz!>= zeiZVqkuB>-rtrRkr?f#~#&tV8BW za#Fl>f&007^v!9L@<|aib&*BAz>y`lh=v`=G1rt9BL*j2BF3K9+>Gdzbm`4cO`|m% zVt;tkIz-RvM=bnXszmIE_l$TGTytGhLKoQxcRPuO?cN}2%_C;4cR@uV)Rqd(e7v_F zhrN+qF6IoI=6!FUF3KX@KO=ruNHc#{MD9Aa{$wJ0bCJcSqYNt0)?gI_!diJ*f0jn;hyH_Z|2HkFV@un&$h9AmYrNk@SWTJq% zKc5+ytty4s(!3*W!%$>QOsm`_=@C`Fe1pMiy2Bo)a{lNy0T^-dqL0$G#BuHyV1wzJ z{F0Q&NvDMx^Y?Ly+8fV(+f8yvR=fJSVT$w^Wp&EkqyD^Q%~bIN)tfkY4F{3}hLSm( z+?6QlG8By_)zLE5sTG*Ka$%#;DVy$}K_C%KYbwFS=M@$e1)}DW*k2<(^}sd+?H~#* z^k5o-7<<;?ylgp<_F&Eqfx84ffSmId9px&mn3Wd-H}@4fjg**SnzjT1290G;9hZ?a z?xbeQo0F|zEmO$T`tznj!rv#}8Aj{$J(LI9<+{x?119X!f|R^s2rFXGim2%s>~0V9o)$)5aBq}CG*S-iVV@N^Dg=_|Hhe{0|7uru*R%{FW%dMxkq-|VK!`}!9|koy z*VYU%#4>MhDR~JizOnv82k$f+w_CbG+vRK$7h0{&c1_#;9nG6iz%M`kxnz?Ocn!P$g1R?PU9%rsqc2~xaoq@e zC|)wbqDW8@oi?4(JJ(uGq!zBjw!!mKfH-W~w0h4ghQiT3f6i25R)bD@JR8T{UgFOD zesU^xp=M*!1?A2jw^VF7C>S5~y$Yo@v{#(4*-X@Wr6SY%K2wRHd5#(Gm`r?q3o>L( zwY9u)F)F>zk>4H^(JOV*rvrbLB!{HPOvPGh2&WQRcdfXxD|c#~t|scrb9B0*0aDF* z2}}dZDoo6bcG-S%QXXvSrwWPH4_B5V^E$&D-nOZ;=@O|yIw+ALD3f3_6WY39H62!& zXQ8O3Zpj65OeMr<%(zMV)fRS11=`XSIBWyD(OOc~iKs`P0BmRHRyMWRI<-s+#0oI*KlKh^~x(P4XA+huQWgSg~`P#rHeK!v7KM8Ax-hC7e8X0rFCyAH~x%{(J9wHQt+0*33ISVaL%>33$`jpn3+oo`zVJZ%x2xT^q3m9l{t1PzQyPZUFQD=c+Q80hPDw5p`gl5s}NqYnf#*3=QbAGh}P20?k*Waky4q= z>P`qN>hUm*JE+uT0;JVujbg_6d#zx=b3%=nJ5)=xv}Q6lPFsveb-yNx_>n&GL~jT* zUGHQWbB`8jl8G8$0Pf|s3Jx{4Mxy6TLz4c!KQkiGXxFwu!}7PO5*e`f>W=ci!r=Fx zUUQJz2XUVq>07JNxf(9tvHNBtWdGg>52{b`%6Z=N|39+MvAqs9+SYHQ#x@(R%6??ZQHiFvSM2ePWS$H&bhAVKg?&2F~_+1_M-6EF(FaT{z;~Z{z^sD7wU)y zNMeuhPs&8QeI`@ox284%9|LU@Z*N@}aKx*(K@m0pALD#rkQE6HhaxSF=^}EjR%;;7E8Bx&k zD4$_#W${`~qqg^Zl>X%C>WV}ww`?(UvoC5ZR;@GysCpU4(=HK7#P4uejjK`gw8gFu zCVxP)T|!pCAn+qmyya9C`~DFP{zi?spWNsVgW|F29q}nxDlVs{AV;p4dzCvrB$phS z+EKT6+8(ivs1`2SZLg_Jzq{?IT#6UdOFnjDZoVn?d973t&g{Km*SPcshpT8+viYH; zCrh>k4e)R#oke%}_32BGGlfT|o8;?zC*P;sa9IHG7Y{yna^Nt$?O63Y-fO|R^~x0>%jv6W-Gl`oC@ z&wi`BnHxE;FT}4r(6x3BQtnD{G>_P(FdZG|hD71KSw+_y0ujl6?g`(PO_;lDTD{AS zBmE;#p|$@MhXkaF?>C;|f*E&;WFCvRa%=OB5ZuyGZuL9y*PSJ-ou&2LE^HGUcH~Hj zYkEC0I%~0c+s~|m1Vwo?p%Dj;_t7z}v`G0;;C`XUa~0F|SM>8&ku!HMJ&>qL;&|M`dfm;EgN~;2ETh?_2|l zrblpX(o5QyvXKTj)kZ$N5lA>W0Xov{1|+Pd(FhbWyMSu<=0UjY3m2C&O`MTmB-_yU zx9yKOh!|B$0iCOO+bfO4M!nxcywj^DG1ILx@#IO3!Y4J;H(NRC%JlUpa3&DtOH`}L z6? zeKB$@0}4(y#njh^zM8}39P}pjSZV*G@?1P`ilK# z1@fIS!4fjB9EU|vxgZb;oNnjH z)V1&uAQ1a|p0UJSyKpgig|3l{oG4sD*1O_o0*wBOG&zbIR)=0Yo@^H1ig|QHby?HiQ4IKMnk#EDVy(!E-^dYTJsZgeD86UH%Hcbii z{GlBxc4DMwn$%pq^*91|N#aV>*}t7`aI!e*_VPOFo9`|)8#Pfdi5fw7GHNxeYVT7v z{dhe3#-jWT`L|qv!f;n#RW&~mbsEeBSxT|k3s4p3M$0O>Z9sQbBN_0PcU(#;Ij=!6 zm3ch6#Y3Btrt=zP&Y@UB;{M+w$hbsKBQ#friU@VkWzygBAWfO!nF+x;TdJH zJ?#W62yFdLQrWZOP5HCIt#|;!{l-H`uMHQ>vuO7tYSfDm0R$X<4XVp+`$P}w4=*($KPxCO-Y}P=;E!jcA+<-l-ol(8#^{Imd!Jxl@ZnyZU!3otr zh4(`X-SJhQmH`R#1z|@bedCdl->ez>(c9H%pKgJOvawpw_EpLa?O*%Bair5j0!*} z7x?R&rj2XfZ?U_H11QKAcoj6xgnLx!yR{`^i+>mBfkyDcws4-ipYLLB*KFqoBuCI& zgO=}C+z=VS-4CuVKgPW)vGm=`)tuu9F?e77oRe-#?rmG*Fe1WY= z9>aNowz`k47yP{rSur2&d)+rl*jKxl(Yc`3I=Jg!?7FShFIet%HtXZqU~5DWz7t3X zFYw$HeNY@pM|%P45WY!TP|ixU(-siwx}edCmcZjaE);Y~ z!EYxQjh~lQ=<)(dU|lwO_+eRr>oF}_;675VFKnq`A%h=N2PV4!5=c^IKrp+3M=rfh zS^k zq*bOf$Q!_@>fAp5Af2So+^ZhA8mcU)zjJqqH(5b8Ve@S_3_MH0&k6;W(4qVLihH=m z?;hsNW#HZEA|{yIiflwvxGW9$bxkG={0bE|tL=+IHFEsFj*nS|DzrRIoDb8^v1s7&Qceax@1HkPT;IjC?8deCI5W>&lhS@iYe^mho}r}mX_Xt*cw{SAbEN$j^X zyonQlEEj@ZfoUWqIgUOLkUSG+UB*N0aRC2L#};Hru~`(%RNVH||HvzoBtJnr6_6>+>|VU0aVfjjVW~^dsZ*IRr=r?_sRMWh)wFE8dHh{-@4V{#yVJ5za_s9th4<^H zUvKdH)id9*@2;2cmrIWqn438V!V!{aAs;OuUmVDQ${q@n5Z+USCkrha{$fO*-jYz> zcX?!#c>LWHw_e5nQgz$RryzdEfCyQ%85O#~yo&5$lerWn+jtz{##KSqWI(Db7LNXMSs`aC?YEqIM#CH5 z02J`-M_U_7!A=@{W$-7GKv2bu)Ye8u_@mZ%wz;Aj5hog3Vdu1|_(;37WQTA9S(AZd+$1+$ZY@Q+ zQ6WQ3YjnJ{WPyL?lp0Xy$x>U7r89Fn8xryt+bmOexAikM&ETaBahG?2awD8ez) z+b#f~bsZx}ZKs9eF`=FzCJj0coQ{a*kVwBSVk(?k z@=zaLkn_%u8L?74xrllg(~2lkilMVP=a|84Ir2IYDVF|SV7nZG7v){F4GTp#qWlL$ z*>viY%ebib_oEJwBV=1N=g2p0!Y>nT#y}?@)R;gLUcuK2bw?&XkBP!F>z$=388o1_ zr}$}V%dwZ_vQ-qrO_3@(N;SZliq>lHnx|UegkYrpap-wJkUrdbh`EWVmZvg09Yl|C zZ6WD<=G{?Gq~EYUbDxxfUKCgx&JCP3=<66{8+6{#Un7U;fct!#A#(-9mYR?lndfu} zE^gXTRy!IT7;MpzIe;xQu&tQsb^?R6aUcn~k@IAmQ1EB~>B#i8zx&}f*yCvsTnb`a z%kGP(DMHOnIKB(mx!DKJQ|4|W5sD@WIv^uzIfWFA7)x~gqNDBwhz}uAumJm5(iGMr zgXqtgO|6^);x9K=IRCXU$C6TDNe^wFWn`wTyjJNQv@Bn4wAVYCZD%aobJM4B#i8{k zqfn7s(F28H;C5})af*~C8u&;iqf0l=*v36sj7rGLe{Dr+m;d6!R9otIBuk2iMO0Az z9;y=G((&o){R0;k5gC~ai3#W+L-7}4GKI+w`C@%x9qX*s*xZC7M<(+&Zdk8eilvwe znq$%WesW{3v;Xs+5eZFNzDPc)+c4TLH1LrrPFi<%aiGq^H2f^pGhe zV#Oc^%8(52G?GPCHyeXQW2QM##@{9T1vbz=n|aDZNKl}U9a>5->)%WZ$&nKvY+mYl zW;JMfs=P@ANqU{!zv`%l7O=e&4cO+sty&}9ejH8{EGxeyYSdo12Q32he@c;1v;{N8 znfwkLt@gJX&oyqdyp#?K)$F7UAYsibl>6^w5k`od_23uvsOi27j8N<-4^yE=7xQ-Q zf_5kaA^T0|3&bR^tpt#LrDf-NbhG^Cui#7^U0n{#O%aqvFp*aK#pO_H+8+M;0WG9q zKP*YkM~+;nefSt$#K3oxea&>6UPnQbJ#CQl9Y96Ec|Mo@#?6iYES4mtiO(>9FLF6UHKb7K^prwHe z9w97?>!yJrDWo7g>2Rp^HLm*GCci>gfyBNAUU!8AU5F+ES&lo{?L9t*`|efgvZGNr z9k=NoL;_Y^>oMoqO2)(8i<8xkVr+zG#N4izW3e{J>YCPc`jtwvw(|K;l$ zRiEV8eJe|s!2`Y^uvW~JwInlV6I}1ry1g&A!@2P!w;02G!U+FzVKcKRs8y_}d=BOr zZtJ6RZ~{rb=n@H2O?AVL2PmA^VR7DIyeCz+8;!=;@xy8w%H_#VgI?GN-jT*l$xbJi z)trwrOAzRCaeE1`_etE|2itKCX=eL`LAmP&S=(?#kHG83OUlIacBWTNcj-m~;Zfkp z%d^XSml2IA60AGki=tCtW^`xp@2QhQaPNIl0%faw{Mm~k@_*knC3b#J5W>4wZ>rY4 zA&4f{A(gw;7^tWi+iPCFiFa`?q)OBEW$zm~#*tA|9m>o@B*O9BvCTjHV^JDZ?mR3ILOft2(c#JopsFJ#Qthyo@rl_1|*hISQAO{CSN=EWQ zK-$(1kZGdUl@Y58*M0CsYGTTP!`NCJaf?hKWf;KlvB=t4s$&@qbM03c^YdH9HwF(7 zET36_MfZ{td?=gVh2dRct|sZ{|3RLu*j+6nd4X}8D}u!$69CNw%~EaOR7{&JJB+i` zGU-Mrc4DqX(Bl)2*_=8@X7nIn#6MZpp+hu=u=of4G^Zu;Ex2#I$ALsam(cwFjCrQP z(~v)qZkoF+rUoRBx#6taTdTigWX6@k{$r6Wu%_4>dy4>1W$U=OE)rYcw+i`=VCko9 z5%8i#ELyh0cip2E zkmh0Z2>tH#UTL!AOg|FC2eBt3sf#M|&Up=7+yeETXoS!v0g&52=hC7ylp_`1MQ`=@ z^V=iIgSuDgH0V(PjO;n|FjmcUEVj1|$3Pv?Xu6*z>Pc4P)Cf3CNu?cjoGo5&)JxZ!BwRkCMwQPi zvd`iZ$blV2o^vMV83pQsTDy%uX+VZ|LMVY9P$ndwqgcX98#B9Z?~u9?pGX2=32kv zRq}AYS_K|1-C6RszDohhfry%2eLx~4cC7jlLt#S8q2w)gTe7JDoV%iK_{MbOn1WpT zfQh3!y4HwRM77JGeiA9CSVR#Cywij;I~d6h-tssyDiu9 zU6; zBRsJu4D^B!%z5mWaX76^2l3B#PwUtimvieRSd-$4U*lj0* zp$EH4qofjnu$+yTV3p&mCH zn_#D>bsj2#*h>9uviG))Dt@iU4byuX;+g%VV-#_aaMG;D3z*Yjn65T{*oNE^xx?&4 zO|15>Yu(oQ5zlSP_iggBTjyh1VyDDmJOg3?%B0QByA?LjeG)b!fC#_==Nnb<@%C!oBR16ZjE@y?Awiz>-fiALHmF&R-r4?sdNWI@tIE z(+}Z9WYncwrQ8wO9Zfc69oGb+tQLNwcZB?M7?rW@r@IhAC2cAou?sWiMAr&5sv6eU z3iU}l^Rpd7+X_<5ha9bgo6W=kz4|dxa_cx?2RD(CZzIU zVtS%axWa*G&565GGmxEuLb8;yC#J)GwXosX+Ja58!nH?yNwrmKCz9Lp9mTR01lEB? zvHo}o!X6X@ZY;hOJrNSGDD0zM0t70$0~Bo>KJJU%21Q<3@{Zak< zZD&v$rXYPI^Mx}o4kq`B=sm=T(()H=;+9LE{@;YRZ2zpnwxhw;<$QFJ}a4KTX{Y^TyDzy5*@SDE8x*8Jqavc*pv;lzq?trsUXXckC zO?7RWX=|q|!xF1v=+}U7UWe&Cg-_QRx;S6Uxu^ce2oT>?^(E;U(22g;TFe^hjew3Ki@aqhoLj|}&M~=o<=fv<1bc0T-Tan`uA`M-s%-BlCI=XvDvwSetq|3w_qRiYif zr9QO7A4vMD78jg8dT=c*d~aBq=L*nAa*;uqDQ8OLh%cVe*+?nQj5Ti8%Fj1Np4~_< z{bQTu-aw0gG2do~Md^f0jnh$V7xOd(JZOyC^0MJ@`VG}J=9m4e78nTzQLcb6b-^55 zduAi^i-h{SBi{@Vz{#}HuSM-z^PZoAkAHhImF{fZ`n5Kr^P}%I3 z#CEY>qDL=_jn}uWb)xbNB{REgoa%J_t8?w}zx@n=YxMyrl>h#NA^d;V=q_{!o>U(Q z2pnM2a`M-%0~LGXxFk9i)aws_ql0)ksgN7+zxmvv1sNU=#``GEE7JH2r~icF%2Ro#;DvhY{X|wDT&KxgB+qDTbBbU z37(g${8L@xp84mu8<}y3kytx25eC!*UxAa0YKuc_dIwh%e~CENK>k{b5*^*>tF%uE zcLuiB)~!WW4*t4|wzZ+IiB;ybxQTzI*y&Cb=8hV_u{*(Kf>eB=r%e+z@r0@BmVf}0 z;r*~=uV;xH;VGDE5zK4+@8ZecFAnS>m%Z(wUeE6odQ?27`&oIPxN;(OIrn6yc z(Y)l6o-ZoIV{DSAN#_8&w!Pg-F=tFT>`(xz=IYTaPr&Ix%``0KN5*w2GR`JZsU?oXdfI?u8hW2m1EQ%21&Qt8i0x2J^c(_iTCpnvc7NSJzl} zk2&w3*Y`WGHryU3ed%?!?HlY?>%|Nd$3N9LOt8DmOZ!kVk~gE08v2flw>&$BQ{{&P z!k)oi4|Q3wyEK+iL{;8VC`upsxG% z`99JY6HTN$*lliDCWNnOA$nUUUE|7;rjdv)%J#pDY-P#WV0e=DtkG{OtccP=m7g># zbGb1uI@?TLp%!)q%5@>D!O#DU;IU+V#JnT>jnzykfKP#`^=*5dZhH$>3%TAVG+zbi zUP3fo$^6H7y3v-(U!W_7Z-&n@<%AJ)s6lNjZ#Oqps1H<$*K)6UV{YB(^x5)CH*KRX zi~^z)^&3>zbwJW88s`EZy40Nq3LtM}&k)A=ka*IW1fg41IW)|jT z+ch1G5j1nYZw^erFc12JeoNKSyTd4+|nlNRdm+4BXig%IL znpruNa3|eLEbe%cfr~Y%r`2rxDm}@N(5J`TGQ7FIGg!EQ&aEk);|tlXi^#f{qZ>Up z!W}Rd$`)QWcGQe}lO?{8cMm9U)pQ%jO(*1B5JXR(q4N3xN=vN{Ka_P*bN z*FzrKz#T-Fs+iyf#13HGh%4K+9T{=^E#c>U_;u_|ALMD&dMVhe>!%{QnPfrfsGPop zw`x=)!(EASR~QNJt(gCrY~0it%X2y_k?@G{{F_ay?dcX73s?n$(Hdq~5!$>)z8Ia> zfmr3>u!?$71KN0HIAxFWeJeawlKLzD&h;Gv*jscxgpOG7lp$Py*@>-7&O8{cd22)M zr{1QMZpy#<6gpirA`+QO_UMy7MNZP!ao6s8qs1h(e7|?2cwD>A7)#=57QlO6{?0D~P^oo_Ne}BK^T=Z&;;pzbbpw^} zNs$y$)iPqGUxjXGYMSl~Zpa66KT5?26jvb);p1bXU6) zwJ=;Gf5GX4I9A;8><58P=$g>fStD;uCIiy0P_l}Jjg_AiT*48IIW~$$nWd2=l#kxD z%Od4D1PuQ!wg}o;6lX$Tc>VeOkV*+dLr(-s-XAb|3RIFlFrQ>9GDFH2?h=!8KO&kM zC+q0-VLbcpM9`A9B-UxWav=mrrgH1#W_)?H?2~-(U3eU!hzo&J=1F*myo6IA4Cn5x z)WOnblQJWZn&oO@TG<618AALagoIDbtGFgxcgA!r-dmcQvMjA6fdJ=m=OG%+Bourz z-mOF4ltIBz)?Y}UA%Wy!c*VS1BoDYbE@`?J_rAQLz159A#PQLrAiTCHI9>tkHnZ7m zv8y%^o)1jy^bHU(yPsV~R}pj&NFJQq)#O+%r&)Kg#c!eb>qqFQprRBxP~I=+K$4A7 zQ!daFF(oQa*_o=0fbw{c>*3e<4=Q}h5I+)0TW8ORN$1o1j5Sg9GlAKIn~NtA4k&%%_`z01kPpz5LCBxP)6ck#cy#unEK$?l!!(io zai1EM-~bvUVD}B7o7Q7~uSX26Bb1yEV$R2VHi4deNM@kW1Yxy&4W#y;YenxGN7D@I zu)>`O`Bx6PHX#V}i9>QlNZ6ulqET~$@arKn0Vplune}46*!uelddyeH?4PVXTO^~= z`_e*Nvv=vA7(v6#uXI4963^_}>)m|Gu5c98R-paL*2%mK>axIEq53?w+sUE^X-&0= z%I2TUCPpbMU1=XkE1$LNVF||QkB$Fensjf3F1a@T{#eaDdgE^!HXr?NNBI(0O-Rxr zKV02{zh#cF_s5Pmw-TM$<<7=tK={7t&twbgR|JsU)Z4cC#Ur>)y#?91RtNW)NWgPuB z<$!Jn!Vy%zc0Kwc(@lZC!j$8Hqwz`_i4t(YgXLCbQXxxS*e-ZRhk6Bv2j}~aM0MNG z-ijCYm*AxS*!da5DNh7d_k-_ua$nrhal+#QNd`i>TkjXTH1X^hRPWz(M$?>1F>2qu zd|FAZ4s}Q!b`>t)Y^PgNM7mZDlQCLW&YX9*X=<2qeGoM@hyagGgJ}n~4U%mdpE%K{lDzuN&;MX-~- zr7CYX(&4*b)ar**&I^WXsp5ibt|+I_^c{#D@qQC;--E3Q#RN@gcCIJV=r^K%+js5+ z3zRF>nX6Xh{49l|ukL~*|7o@)5iOuX=^~9+WEzb?oisIB?7cviXqUW4mNAZlQHw;Q zuYUula=r|<$bSjLW8dy%nDscqy!=Y&S*-$dPh~h?O}WPn$K33N7n4h!0tHU>adLpB zCs{%RI5+=EqB!R=yIhfKu%aw}^bXMb{rTl?EH2+T#uvJsOkcO)9i)QzPjn?(|3~8V8M8MA=hC8qEoC-w2XkRzwpobW`dCn*glpKnylXfezM=+#kZTj_* z(h;RV5A~Cm ziK|#husdWR5XdnD(h!jC`@+8J^Ekz`uw1{U^|7zNNPI?kN7j{(( zbcHIJ?utg$Ny7gAL+If?Nfs^B4cVsPQx9!rl~toQ^i-lEi9s6#r_I*x(t+rPARhHO zDwwKJWf{KdKk3IF4Kv`;HHQ3lbNN|P+P^X=u{JR z-h5PqhCJvDtU>urbiUx(J)>8DuyMcfK7K&6?qQ9LRYt`dvP$^LZ>;`LC;}mI-EpfH zw__KpfEU=CP$0g2t#al6@%!kGrl-XoGJPksNl+XV0dVNzFNic z3Hv{Wz1Q1&EcHLbewFC|R~poT<33(qaF<22csjB+R`LrWr=mDiq)l+uNOCG0uB40* z|3FjYO^IHhZf?@%F)Ds+95rl!QcBw&(TGG*Qz!ZeH8f16ZvbU+G>_njp|2k8jH1Q4 z>-)b)nI2c&$5}p4E>{@W0uQi%u#8IrApDEbD!JEe5FgexTJ0YA`oofeC){?cP1$?M zg}aVq?p3o$Iasl7Zj9^k2DCSDa`C18ZSg0_HP>545_)SR|dXjl;zjItoZuj^GPo1J?+N6^bp0qp$922>`s-{C19eKbv;zTgs;C@ag zRRi^1sghxhg-cnL0q_EFh?SnESnv8WA>8T#T{axI2CcP&wlqCVOSyr{N{j)`g#a49 z(xwIWC8N5mJRcQ{FQy;U-(;XZXg%fPlH*cdMWDPLi*;WYk)LWcT|Rdcqi|&s8RL;= zN`4;x(o{6Bgs*nqdgQ`BpPV5RGrp_sHOga6!Gt)*3zpyTaxg4rnXdZb$ zu%HaDPrj4~#>4}XcyF;4T&CA)I#&Gr;RBE*8~t5qB`&YqZ|!d~D#HQ;7^JJB>9^9V z{1x3th{7n+B^7JLUm`0KCB9TwIDOh&2>#HLbg|uOdfG~EpCgft@2^^&3tKFL&{ke*3Qa_L!aGw-PTn{(GYjK zxMh70Bn9i7iuqeDd(~*58S%_!ax2pH-CRMiCTo#dBTGTwg(yZ2MW3$uXEOvSG%Sv9 zv2=G7Q`Q2#A-eg{*@$JC>r86Wh2m`AC^GL40k1S>S!_I*W$+0Q>{PTlbWC|I9BFos znaWt5Q1Iv}FgTrVdSNJVa%KxP?&EFjry06Mkx&{(^VgPg@}2r}oo;C{YWpfgsh~kQ z=lk~}G!LB)j1)UAf*3C-NPAT$=ie##TlTS>}c%qebx*BlxtTQ+mmiH`rJnY=zk4WWd%K z5#M-X@<2F+@e<1HJF^1j6-ShwioG4DfP<<6u#E~@^oV6Kssy*q?T5CScG$|X64pg= zP|EhD*ElOoz-8quzRQ~%wzo9bUiR}iIuH<=b^1HAT3oY=---a8Q&LP8Bbu1vCk6%B z-rf^$R~gH~LO}3e*SQl}H%$a(*SxVrPe*#Wi|O`hGGKyu#*ii)9Jv@yEQSrQQypA4 zh)e21GK!`e5%<*F=R*~(gPz%rX46&e#r6CBPeUa$f6>wx2|48X-8>Q!B9D8;7;r|T zklCd=1tSLOE)=5iu3UoqxMSd(8qLcsnvAB4ci7?$riRo~?#T;nj#;sBqfVlu$yH5s ziOKNaV)mF?OBzmVKTtZeDp5Qcwn59bUlY4eCN1W)PyNC=6Il3oJBAD)$#< zHqtzz)sp1&Q9)47ixlnGj9W0_vCHZ6C9wr}uoD$QG~T~mDB(_)3t2lUrEz-Q%#m>w z`hTPO7^CAfD{v`OiGrE5a6^}+^WTFi2=VyM&Nw}Iy0E_jkDYM3CaaxWtmdE~X#&h0G$~3Y zHX?b*!v1Tb9#7xeWS-1Q^)Vz-d<`K#QmaW34S{)GLLLP6O7cyMG3|XpkmZKSheVG+@LcP)M!j@%bg{Doj8u86k55Q7KBdumeQ?`$kQ{ zNg~Qz0Z2sO+(z9EV@S7?;)I~6AKnwW!fXy3g7(D~Fuu@2CBQt4uK~l!NOEG~w zjTFq>)Bs92c=oV6U>ir=VH_`U01?gOfvcGJ&)rzNZ`-@qBvPQj&q$TF19JxO$RVvB z9vwo`rKDFLL$Hn4@DCk{x>7Nj(=Qr8l3r2R;LA&4(TjKpS)>4a==*s1m^LS!EWCXv zoqCL{B+Y7$~RKzp*;HMKjH;S>-7HDIDV=9vc(Gxq86qZZsQQI4%+M)*< z$L${O&`(!q1%7GG2F~UwM#HN2C*|(`Bw#fTt=PQ{r zI?goUfT9o#3|u~u#E@KAbR=JK3bE-Y;vsrQ_X#rr4O$K-7I}X=*S?w^B3KJkZbC?I zNH%ILJFuyM*Bv&t#UagN%y5j+3UnFfJ;B`eZF?Z2vuz6dCZywj&*}_EQoKq^`ZS2H zSfD5{m^$oIH!SNoRGB1BH7q|x4kMtl1f?`Qi8~?$q>9K*Ls{U1)}plU6AOa3Du;>M zBM2gzxMd+WYI#CHiglL6(!>}HEL*+zMBFvC-j2LpP#KmRrgq})kuOWi&=0(JG%PjX zYzt7rYXC^(Z>BMkCJ?NP=?pk!v1@;Dbs;<{DUc*2I4 zu$mYktPtq2faeqr#>Y|uPAFD~;A*3c8I!NDCSYE5kn{8z-~UrTC2!P5QL&N0rQAyV02TZ+Riqrj6E^lbGbae ztZ^!;Mv5}aagxxVyL~vEuiR5kDJiOz@+_N=%?g2=7Xqk{qRA9Cl+o6wKsiuv%8>={ z5R*_sA-0$jAzpbzjSeDmi#Lrhg$B}-$=5qpFp6Y^f6S|q%yfmE3rLri4~HvX2((xe zK&ed5za2RiP;PB_^ezsLo0G$=PM@f3o8f1!=4%gH7a-$Fx6IwsxRA``R66J6@gVJ&wAaQ ztr;58)NKVlh8u+f=ODtroI&4BoE4TMmqOVPpN$!?Qf{$GMhXW4;R1dNldY$P$>h|H z=ktzgE)ks-@)~)gc(Js{a$8W`8PgFJ=&K(Ek4B!J(S&=uEknS@w1NQsIMmKO%Sj7H z%nIn33FgXGLOZ{-rm*`U>41)F`4>1%fC40ifyj0NDKBlhth_PRg7u$ix>sVb_e)p` zC6F3~HMQZD$;STC!92koaIGzTtln2ipHe6U6ZS%q+F90Mj;xt7xncIifPAb_X42|C zM1sg=?nnai`I89)k#c-(<3d>Ie|2LfED>(~+FU8;0Rxr-TXk{Ynt>EsXZEbIPSNFRf)@*yHZ7G*Fzpq^ zRTO3<@u9z0L?=ERu>lLaqI^xp+Yjx+wP=VHBogwq+J>b=R31sih^)g_Tdfx09+g5j z664zsr_0rB$||h?K3%G|k_dC=SXN}P+HE{kG|Qf8EOGNQf@tHl*sx-j`Sp^cf)dP? z*u^Gn{nQ9j((`E_Z-dx_w`r+*C6YvV_?iXRl?=f51?d*mIl$CKiFBM})|#VJxg=sG zc^t=y{ctJOpva>XA2jxuEVu=Omi|GWS+EiIntQ>(Xqis7^UnA1>-FiRf7Otm}Sck##h-T{^1JqG$w1QWRAZw}JYs z*jV9;pXuP?Qee8tWTjd`B(xA#DIGnC@%=&^Bj9r42#MQB!U zC$U_kXy8%Tw~cV{cUlnh+6!6F&`UI5eTiBlEll&-v0=Saa=1)~eV_(JanHucsv}Z) zGskpkPgTT5IKuB2zMA_zpflWjSZzIaO)0YN#n7XGFOZ_WfK%`Kq^(Am$En-7Ykq{= z5#w+NJEnzq693C(=3-@%DMcCsw{_#C1ZE^p?w6@er*rT2Nb8))RlHWxN~mwc17KQS{GB#wlo7GHIe$g0l~8k zpiXBq0XS_?)C^cYNnj#C*%PT)T;ITsp+XsNY;Ua9)b(2pK?xQTh>{faEK)o@mB8WZ zDc$OF?Nv(?IU9b~{#j9}lWF7HO0wn&q3E3B#n(<%FmlCRTdSGgjn)| zn>*J#h=fGHp`(kTz;;@gZCf$T0paWxzyax}PK= zwXv_f`iT0C!EE74+8@(F;=EXjvPsaQ&^|EC3>3*Ak_lWu{!DS>}z#cfp)d)@cnA~EA~g}mI*q+ zGaR$Qw^}Ae<%~w~ZqN87B-4_Na>B0wWg^ znlmf*(V8=6sj+qL+}_1G+tDLY6Ttv6{N|)D72RoDX>prd&}MyGrO9wB&x4KP)Ot_C z+;kgmWz<{3BJS#@mqA-RaBhfx>Va*CzDaZE#Zk5r+gZ9?W&^4*Mh4*{$BTbm!zXxo z7@tJmX%E7V^OPSVAM&+Bkm@u!GR;*>V;MKM{tz7!6ECz$u)81}O5E)m;5Q7pq7^uM zJLy-gZw#wZoDWU;n6-Vo1`Kumw6Y_;`0v5S8&1e@q~eKkn_7-JAS-+RlA);{uH|~8 zEYf{%Wpn$_9x}tnG4%k}qas$Ayn)`LM3vP@&LE5<<6)umvE@6?vxMgq7Yn!Epwn(L zSu4!n)r|iKi$HY0Ul*cAFVn?>9Vv&JIksZpB@;I+V4annwp=eaa1&F2`K;KmK=bT3 z)OFgG@6GBUyL6`3fV~i*u6%z}ZuiJ-JY|o$YMCrF^Wz)xEkQ-U?o@Xe`5fhShb?z1 zZ1zTCsnv>H)qHo`(y8d!=6O`;>_kZJB~p@aD^glGFjC5yXfa`GmaWf$Xa#=5 zz0$y^h@NquVyF%k#%Kj$b;%0olgCtqCm48~U{R%5Rr&_$TwbT9a)<=-LtB0%Pm%d% zT_l*%FE;3Jqf@fC!#VnTh4y+{p7qEx)Y*}UZTYDXUlvi}%j5CXqNtO3jW)WX|4p(4 z49MqI$<(e-4#+P(@(X|7^^UN>mS4#WRG={|50=pMGIn6xWAd_jDOf1KmX|#88w%NX z#+H}mx9UJtW0=9N`KZ27x^*UjPN zQj>bU^_w&P!+UbSHtF#&t;6*VapwMGkL)5^oD;M+G2{`=Y>Ie;=9S_@?ZRXc>A@U* zH7=++guMEQ+i0OZz8^;ad6jR4Nx)g))cuCkAUNic+E|RJO#!-|AwLJW^9%;X7~V$Q z(ARrDeV@dZPw#*G=IKR&dGgLkDw}N=2C-L^9UqKh^JIqTo)2)=rLTTDQp%Cz`4m3$ zD)(X7ZbK3@i(LG!f`u6v!IqsN!blv$X8}gxSW;wIj#fqmt4TW?u-@H|vUU4VURUWq zE{$=IV$zaI|CCH?X{Em^(+XLY{%M)!Ud*WUpO78StMq@*%r^7=vqGL=-V-R@kI*`Q z^*+=D^U|1~#-fm?3$^Z}dm(B6l& zp+WnxVcnE{ID0S7@o(zFX1~KP!?#KK=98~Kl3|{k*usg)I^M#sJ($7ophx@hZaV)~ z{YvImv?+fps|(we>Qg;b7wOk$^y{;Rbi=x!Wn{TbLPZi9ydRfSV^@Uy9quu~KpIzG zk3qpfX?!^p=x`4X2Ha_Ub>>j_%#seUS5k6miI|a+p8q(-YjOXD5?-!!cLp~m|gbIVc zC*cbghI}0u5@h3c_V`HSeNOMeIpjCV|L?S5O$Vlhe11ubyGy)WShz=u0x~q{dq96m zEldi?F-kQl=u6A7rga~4|sBWuFt6Cz)a832Y_J zOeXPJOwY5KS3{V9IZVjasKH#+@_Py9VL48QgN10uB5c8_XvJwr;dE@rVw{gUTuG{H zu?)L#CT_=BxC<-seXPPOSdG{Dd>emj@K>zGzpze*V7-(QU5~^@DZ@E38JnaEVVQvj znS*AjK}1f)d9nms{4TEt`n;&KjJ2hp9N%%jhkfo3(Pa(6gH{>#Ta)lzYbqYL zrsEN7CLXnB<8f;dp0GBe*=m2nQ&tOpVkPjjwH?n`7vfna>YrK<;b+$4c+UC}p0}RI z&#l*RzOe`Om+NNEo!|h6KP4nYrP|tGKm$_Z~dM+E?|v3#(Ghvu;s=C>sgt~ zmW3Lti`p-s=hRyF$TYU{vBbI)C$ikW{&s$Slqk$qe@yGMlZza=+^i32{ZSJj8T9hpizp$30)FWiI?u<35&_EEBta*8a1k9rs*r^;z4l+$_U zG6g9{1A%T0)bf6k+4X-6?z)&hKLeu=(wV&;jQ60-gGq<{0tfLc3>|1%#Dfpa?+OoA z=azVIxp@xxgxZk8A%n_vkKz9f6CJxe`H@N~OU_)( z=E{Jq&NkNuWPP@|F(Bt;o0|f%*);F#26!V3hJ1<0@ny39Epg^6l=D@bgx7ijzS+`{ z!IJv^C@aJ>bX{FrTRAB$PE7}@E8U~CBwCfDCb8HUz>sGWxRpuZilTDRZE@wKQIido zG}Vl9?K=1aBb0yJ2vtsR2i_q2H@WQ}2weY2r1=)J}~T4J3QEdM;4ldmNP(56ITE zB)jp zk9;{T*QDjTL+*Z3!E8FQS1KsVr2_pk?2&k0HZV^nLRP{4cwW#Qf(PjZ8c!EcjW=er z9;tsEnYXF3xQt4@iJD)cM%X8}OfD|&l3UZ#(It1K69$8oOGz2rl^&yERSBc zx>Q8>={DDOl?1ju7OSLaU}^TsbSjlsVL1U>fal6gILsZ*EVx@aw@s?eu_`m7n?P5~ zB;`J}4CmI!Zj+c!;MzS31dO%(aI3XG!McA;IrxC~^XY$n$tq*6u{2P!9}n}$d-q~L zZ%qNYKP}%`)1%!Yt-h?@K349btl1;XjOz8pZoRfrG#wkONkY;*?ac^|tSml`om(-u z(w!OCq|wV|O>Seo_YfDBvf7uks`t3y*sT3L7-dj~WTRLzwi)l?!T4Fj&${fGem3xP4nN^uW4%8O{50`%9zQMSWI)o>>Fq4$Z$W2qac6O9 zXYsJk;^Cde!Or5c&f+nhBZ@D&4WUfZyOI7dqD#6mE>O2e_Op;Zv=bvuEAz@iOVmv$ zn5BK)IeE z*&!%y#MRP-8>JZ?62ZMJS@*LXjmUu~M)#xUea+R>-S@6aWYa2mpIjm$BFbEtj^;2NQplSqWfV#d-dIX;*KxdbZ^i7_adm;%g;a78W)( zk`dU##)>7`Sia-~cv?M4Yp-_YJ!}~WB-{}0+uVdh9081tS28Mrlms_GQc^bd+b=kj>RjxLhY4X!lCx|s>V>e0hge7r?tnb3|o)f%=bQrH~oh**hajB~6S zO=j8g6E}=hyr(N%xiW0)QZ;|IoU|aCOmruy;gs~?P_$CDTpcUR^#X?%oG`bRasV78!S6s1he zMSy$kuzARH0u3X%nN??KlY#PKEKcj>!F+++A#x!E3rx6C;K2l~EHp6z1qRL%m_vEw z!D4FKY6W+-v*tw=mgs+ZE+xwY$8Wl9lS)*ZsKGM9II6tPijk>lyD1Ko4dZBy2c(OK zM2anF3lW*~vBJRe;hOJ@C+sd0b+|xKq!Y%X-K4?35m;muIKD|WT4mxwtR_e8o={v( zlpNiv9bY%G#_8zVMGfNG)M%m!YY9{$x^aE|5UnG2qLM9~@FIUS8(5z`t_ZiI;8d=& z5}k29t_^}&f#HL)a}1T`G0{+F5AU!NRJ%2qupNk&XKP+;!Bzt;$1SQ-Tyz_@(CP9k^7LvwH3uz`-jWwRxLl{cg6BOrb-I7kv9caqY2qq;llH@v+SIoS zP6>>{e-Ew}j9=DRze=GgtvYkz;#x1taIJ~!a6MgmDAH#4tZg5|qU-hGZZuJhi5}cc z%b7iUwhq6=#I3lEoQo%{SR%eTl;|8|hOH`ohlx9J7Y!|JM>Da69zm69u%B@g3#!}!$ijFX5SvW&4Wh;d6B3W`|-GmC-5CI zl)fZK_f1wfN&J?MqW($w7!RJ}j`rDc-O)29dhx6}o83t(93O#7)@g6`;CadrJSw7! zeb>Z`c!^rGUe_54x6vt&>fuh55vU0rF!2fw@?d`{5wzn}pfqs8LCVEf1&dD*aW_UB z&q?Wwv99b1+TDpzG-4o?wZ0*0u1InNcFco*nowgX9%ox=datFSNNFZlD)r(pmgvub zi9f>Y#Dhm>N4WKhlXC8vWomSaH%z?g*h3Xz6XCr9nRFm z;uj|VMyZ$23B33_{K~*DnRyKkt_xdny10Lj6exp-(PVYm8h8J}#6RMnXoYNRh<0?? zv6DCgCp6DWi_X#)IP8qmsE zEE+5AvQ>vtda_cDNk#YspBng&;q)6ihl&5h?}&zzu+|5@VyP3_{!r9C_%8y!(2IXR z;By0?4Li!5U6}YE1=&kL8+nXYhi%}0v!WU%)($bDV?5(%ti!R&xYbkHynM@GuLyHB z9oFOYcWVHlNv?Csx@e?5)RBx?Dw-QZ%2SLs%oma<8BXMGEyMF*Bfcw~P8WovA3r>3 zvym4DcPQfAFHRpV!et^7muYnZ825jX6B{1VDB}t{QOcFRGZ+~IBwZHB@1<_zKxhdnEHN@k%D8sjK&^i%=Vkrk28w!Qyf&}Ugy8}rD;JouQdZHQIQFn~ln8NB zmowFMwJG&_?=U*8c$1@+C)k?i3d8lNs1LG9O)1zG4U%>zrh^8Z7*AA7+8n(>@kr>I z7ljm*(Lfbcju2n=m7gQd8fBud8s!mQgP>S%F{MSnC8*b=XG=IV^aX#8Ba9*2$H@P- zG&VGGt3|R+E;i*7xin2kZBx5<^3GHhM|#bc((>i z`3C8#@!BI-@vSx5w$CHq;u$AmRwQ1Lq!;waHOfOCfsr$Ue23t!41HCK^mS>e$=)Pp zZSrkXZjoCV1JngAr~)qrWy;f< zxfLm6tWYGr#wJX?RH>8?k&rq*dcTnGn)0G#E($C5CDg24fwFBYm<-ARQ(h6~{%$7; zWf|DELchj&o1K3QI34Q8=f0geX$4`bk3^VCXl|&p6=X9&x04v*3}(0a019ZMZvLvR z@)E|@LcZs*=GA+>(>`uU`*_wTWaN2$Wa(k64<(d1g|)L-((vIf@58CDI73czg!7zE z7tY3HzL{wu_%VfLAx_6sN~D*VrW3hVvv(XIzZ<~xmOg*_&vM@xDa?EmWsT*&dFi)9 zs3`YUX5Y^oKvhe*Z&AA5hqIe1QaGpD<#!#$MD6pP+mG|uu1zVdd=Q0N)^DzGDn1qs z{aBa(IyUxW^X38UXsPJKC5O=3kG6hvNVQw43kI;WWdPxp`F)5SM6?gdYGVLBEq#jq%p%*E(+?2w%_v1EyVS&Feh1(0-QnDJ(>5;fU8)X-NJh(SYMF4v2PZ!T*6 zdnpt(=9lnODWYZ`?%*96#}S3p`wAKF82!hH$^66pfl$XfVluU0Jz%16kc1UK=aiJYI3Y9iraO+8w0-okZ|1lHqPThI{B3?xktpkBjjDY$kbeJV?pA zSbG!?)n*H>6U+D!|+eb$8+)t znwIK!8uGtHiBj9a{R^CD$hObvue|y(eN@&qZ~=p@Jvr76NV4T1&g_%DDd{OchzjL! zmbq6j6qLx7{B5czk#Ca6>C;gryR$^c{km6b*#D$Cj#oT)}$$kUDc5%kV4%lwaH&Je6Z$S>?v{Z9Wimu3kc&bu- z9SR01KFIQ^@M`9tNnZAIEf>rA9+aWe3^Fo z6UTl#pTs*r`FsbBDGpU$rEZ6bON#NQk5-hT=^b%)T%Mj=?sDYZY`;_P=E~zRL++7# zxq}IqOug^UP@@QWU((qdOgSe|O(wr-HDx*{(sb&%DK9|(UFb{)Cw)4L&s_bV1{I?mJK-5Ab=A&%=BkJ#Y{=?1wM2-zh11T=zA++AyW~s;4mTpyLsr z+>bM~+o?I(R@S?Wl*EYHewEWwz;aGC(mqI~^#$`@N$rf%P>A8kiEE+z$ePSRP# zw1j@^g_OKJGC%zp%MXX-r$w2w%9BIR@2hf{rH`_o;Cx9H==af2r0;zH{{c`-2M9de zNLQW^005dG002-+0|XQR2nYxOdsLV3MFLBITM2xW)wMt8&P?XZGE(kF{12Gz4+CRcp2Gty;BxE_G>ZeXUOg zLEit|@0-a&K;C=5-+Pai`R;e`x#!+{&i`!p!`5Y;tgd&HVj5QyZ7gmKZfuHw zMeAb0#&~f}G_pPvON3(a;>LJ%?b>insJNB*@-Vk29)=f^P-X{mw2a6kmk-FlA zYfcW;B$(2}k*4Ov+;BsP$y?bzUY&@ABXy-rX8e>;LnslASTsH3(dJl9s1h{I!8++k z1Y>ofM0x6=hSjQ>rU|Kv> z7in(9vJP0ApXsaIt<5?avQxF!@^5?o_{Nhw+r zY*-qMh2`DuOe@oDJ>;MfCJm?lB8U2LoN2;A8~tfzVc(8ebA%~B&k|X4BHU0sFBq>+ z$a1Cm9eS5SjZryidUd!i5==D5u$`K2=an6---CO~yW!r=#PU)%6(kYWfz(5O8ez~F z(WFL-he~Lio5nI_cC#gu#?u6UoE8XT8kJYs6`;w@bnJ4|q)t$_b|gxhVv>u}3<@y0 zHi{q(DudFK2o{oZliWfwgUJnO2yCT$mPv+mABFBjT_V{%*90ukJrCk-3dVv^U{k$8 z$LPr$1;qws31O8Laacle}aabQ})5DHQjA&_r5h(sEh> zL50>e2O9vDtnSXg(oL(FMjQd!J+zWm3;0f8nwobcx<0rkf2nNtB$G0!ubbAu4!SP9 zv9Y1Jdgk(3&EbYxSu;o@ShD^2bqE}J1Sb!EC(p{P?%0})5$Qy z!_k@4K=2Pe0hz_oXd>l*9fsv~&)+5hY@$p94&{M76rpt@yjT}KbP@~}??FAKwcey& z)YF63Mw2#)y=QEQg%hE9p`aEE(`hE1PG=bIJ+SjkDGKkU4h*~tP6sT*lpju{DZ~dp{wW`H(lM?05`=G zp+=LgrR!iP5)7hsb>gIpx&=Yki6brY(sa6kZgkUDrXhzQ8Ce-xA8Ih^Cb}8toU?fG z!o|@0p%_}!r3T%93Zgl&STvSxyI$-gyQ(_da=F9D}m}v<_>6P(>(p%+5NHV>Z~h_8k(#O#M2i2b-Fb zDMIQ0AS5GyHXd0}hAgWS!J1R5f=%|ap418AZu(7UeCt`ZBn|p4)RU8KrE^6Mc0M;P z$3rjDI{EpXNw3mt5JRx3DHN$a0`u<3zf@SfVbYs|WFrx^1`T?fDX(lgl#mU}jANVA za>kA+$;l4cN!zrX~=9r#_Kr3KSP_*sx*DhVf&>aK@FCj4fVXRasqAA8HI1 zg(LAqFj5o3tavyO*PWu{gkJoJ7R=UNMK#e_s0gn`iC8ca4-11}!)ZgZ5VXwUx_G6Acj zeU<;fp>&@WLk;>4cJ0B^I$YX*#-P1`@~Cyq(F9Jj51an#yr(3%{%Fzx&{IY%)YK5H z2`$m(%rXDngsL0av9dQUV$cFCHyGtvI!*jKzYRDk7O z-7-*=YtlQ<ryBwh_>9?Q42vjerkCqnhPxw!@hfi*NdqVO7% zYbARzAVQ>YQOL7jCkv>s`vKoN4!gM?CUWqo$tUwEU>niG?py?<5Zicvh#E~aB=Gcb z1CN(wnoM5DlF`)%;|nxLV0NWIP&+zHqYQ8`YEA4P)pHV!>U|O!T_$dn~hl@uEFF!m0h-1Dpys_YwXFnU5C5BgyS}Gl~0-cwCp`S9AAL2 z84K4K{4CStRUp@{OS1x7DX=TS5Fsl z{L18C3rlxnFaa+f_fUjiF!@Ct?ty6pg_s zCpg=~uh3-%zX`961eRanw@v=NE)&3Tse~Un<2xq5EBR`FPo1xJ;DIce;bD;@b2(zA zxKgCQPVp^rkcVXQ$0mOwmob_4Wz5nrY6sg44gL#`xhhz6>gqs~!JmUqLgsvF@?T|6 zhCK&Ga?oHTtg5US{DTQmlM0Fh3(7UbVUJoWAt@oZGxhp@Z0r#^iuUQ^DkdJWtLQjS zj`E#J-;-Q_5b_?A_liNe?F&SBYI;LR_M7~poVHI#3F*AZP^5FyruyGhdvp!9llZ)G@g zN*rKUgC%VoECfSMHB9o8o``L-n_Zk9Z>kBh2@`&#F0?pQCszk@|1AW_*s-I@T8J@2P1TKr?7h@fWonvbv$JiC zAMqVKpO zdI2U?(OAfy7dO-rxf@GngoZ-$Xe6^jk?3+$t&ptP6RfRGrRz>WNN#>>QWc9wdMq`; zy=bMOQ+Al_Ocx4Ukm)jDxBDML3Q#Hp#WeRwZ>)YQ$0_K3p+nqYh5j45{;%&!zDtdN zFqDLAKzUJ2^mwwVPEidK*X48Nly2&sBltc5y8m*j2X;rSctR6x}7T5{tR6OU%7(}9?ID;CXJTv@ST zzM;;$?o8x zFA;|=+~lvJkf$2@ReSR*l5cgo0MNxK=2z?T!M;=O{P9#@Z^M06L?WS>{Jvq*6W2w1{XV<&u2b9z~-;k*DeNg^_B(XgU%5cawj`PAaVQdy87B z_-UHBgC_gEQ>}KD-&>mOo#yv{PEWRH`n|K0?Kyt$(N=qb-&(18$$YmFfe=Zi=iZ+(}J4C@#(IG)=!Wx6uZl(MG3w&upWAv$s>cmCoBv z&G;?wp5ICrE^DDNfiy2t_lw);O5wJhuJ_*JOWQ@aIjO*VkI)qO(psrSx3)n&SJGbE zPY>A72kqxJ$X&E>otAbv6g~qGm`Ouv7Wrv5RCyFybEuN$(lR=lYG@us=opHjXFXNX zskDI3poMfkEux>%v2-baEe7YMbUmo=qZRZpt)$(wik_w8=@nW{@6ieL0i8%6(Mj|f z1?lg!hW-Vvd#RQVP>9oME%&E79!m9`N2h2+Gy%TQHf^UJ0%p#kN9a-chYoB~D}bu7 zmk((tS`MuGCbgmE#4aDlzS4k|hpB>g(PL=urs??mILzf)Anpl&dJ^Y708~9iPg5G! z9!Ae##7)m(oqmE#z3Q)0tNt3RK99FtN;`mKr@5)bO@22`O#Ppzm!sczw8TyK`2p=s zK)<3FFvW?f4+wmDB(^CKzt8KNzn$hhPyKe&iz~b@?WC9S+Dfmq((CiRZ$a_i-{VIQ zzM}^#G3b5IqDYQ^yst+lx6y|(_z8XhJpB6Q^)D$}@8|OKWpdWv^sH(2tgqV1zLuYV z%Fj3Q^DTbFR2>>5$G}ezMe)`I16)UgDFORlPa~)qX1W2;*$4$}qP2K702=G)G>o23 zS3_1e(3x}#oke%j+0;Vkq)gpG_W)wzZJyVjiuciAk)ua{(tcZZ2B;nYWVea@oPUN& z;jPr8e(xUd4{fx6z8oo&8E2AHXR^aOPo-^lRfW6RwW44g zR_-5gd3`V@AI!*^$wOLsSitZZyLbd5!0mLA&*j*~`HG(A!d*N@_xXKBvTr~j-Iw0V z<6zrDH&M}lGN04uYUL>{^rO$^b6I?9yR!JG8y4ueb8Cn?0Q6@ulmf?l|zerRd`{ zco%^vlb5BSE4p0a{UMWA%BpmBCLeF3J(EwcW~^|Yw8Nfvl0A=)YN0h62D{`QFOc3s z6|&x178wRSK981T@3h-#kk9ipH?(pjc{2J?Pxndx0Fe|#)8W+M5@ovazz(0gz4u}5 z3dERyelN~~w!n}sfDv5?FLV)=!H3SGi{Y;>fp5JO{`4~V&dXs?SHPUE1m#uexfT8LZC68S2a3N()LeR0RZy!sj&`c$)TU}_ zmujNN)E3&UE~LlR<@AKQ6=V0&GwKm|PciJLkx0NmIj#+RxVi?br@`3skuf#H6DulH zn|M7uv4du*I1+~^A(aXwup4+I%)1K7?Iz4fqhnPqpNglOR;iVI8b%FBp+7wKlaSnh zOOT{);k5O9x^7iaHJ^c&2PdD*XJT#!&UXo)g}Lec1Wn|#k@R?Qo@zdu&!KdjD8T3P zdFZ*8W*_x*bKO3FDt2?>Udq}FW*+&BXJ<|KV`(RzGyMgvd_j2& zO%O=?9WbPeEpsgtZ$AX1m&0zZvg3!pU`V0Wb8Yz$b^C!={M%A)e4y6Kukrgl7JC8G ze~|)cPlr6S?Q>&6^rz{BZzkWMwI{8uI^L#HDc=YwI`p~Sr1n9yB@p>6o^9=ao5y2N zrYHQZd{ZXh+RC?A?&iB;;rCWKaEv7dL&jl)x6(W`GPwog;*T9@|Gbrdv6V(+{|{~@ zm*W969@ZHcybcJQ`jks=q@Ea(j#hqT*=7e1DR}aZ148`Z11Td5=sfp#Q0S|GoB%As1RwwRC)^$>ung^A80;*y#qsg7YWMyFt*na4*KVHUhVpkfQu*Gd^h4dbpbsovAU+ua>|g2iRTXFtwEzexO8F;9Eajhu2rvq57w)uNm4$|Xsv_GJ{RYyx zsg0jZiuES1Pv@|1M+-Tf?P41v85ezm^*@EZ{RLL>8AAW(Gz1X-_m$%yZ5BmMR6qiC<#m{B(`xeuG{tJq%nC5Ju%LOMk zW_0MbG1EiZA2Lb2awRi{H9nUFG*m0tW+wkhLu919!PIVQ<$t@8GJI)m{80-{mfjUQ z3>MntpF#=n4ffk-da@?}qJ<1R=~UT^-fN?E!oRgk1e;Hts6AA!2VjWy!EW{==ll`* z<^ceRsDv37djKVWgM%8_Me8_?HnE#dXM-+8I)4p&=vEX7Kj$8_jeF9Q+>2i3-t;>6 zp|{ve?{i=J6ZfMZxIaqM0qkHO8|-E;58+~zAfn~P&?a=J!vfJ@?#o|kSdZWy{11Rx zQ3)0C*Zl9$@;Z7-gWiE0cqji8qfP|&vo+LRK*MtW7r3NlRv|IW6O2@Po>RB*&+=3#EY*sv2yz4)8>HmL-=sd_aTaO;2W^GuAm`FZ9!2@c=Za7b zk3~Dc1vH(<&{8gjpDclI9ShGnj-m*-H=}(IPefjSHyLoB0;8TvyE#BFa4EgXW%M3T zqi=aSGtU6bX999l0sJ|f%X7JekLF1{kIN8&rt>lQ-+W%gmAs6r_;|GI@YcZ9d=W3v zW^n`jG>~MQ#YpO>Tskn0qP{8(;ZRW#Ih9-2p*{xU97{ zr`Adu*A(hCaKGeXPq1eXxt#kc_h3s4=mf3b*!|R72z%}!w^-Ycur?iv)NI?)=ZelX zS_FidDnpm!NlO7HI$O%@&X&?Xkzci}#UQJ2&AYCnC) z9koM$DRL-+0<(EJ@- zyp}%TditEh^bbCnzQOp996@Rw)i6C?UliurFwKRRw}4nkdu$+1rAamrucymwAl^oQ z=i5MhFa^YCl0c+?B!QTMn#K2<6kmaw#cgwnTM}wR$+cf{eZkc9{WPu%x&WxThtEx# z51E^AjC!Et>Lxdlufg4hgtP3B@VICwQ}xlYySMd{iI=T3_kS2H{B0@=e+T^U1`Rc6 zfnySYKB_3onY!ly(%aM!R4`JryR5u_WwVpBoJIEi!rRutvpYz^PGE-n<#vQV#DQ99 zSO=zUDyI{R&;Boi$dgnIU9Lyq*Z@p!qyiMtV^Ka&K;d7G@_06?lA}?}%tw0(@Vx@a zUX4OF$QQ!OE}}Ew&^PnNbOB#NSMsIEmo7%|xSU?$tKcrLrnmVTdWWy2Px(53`a54w zU!xrSp0D9FzKOH>X7=+fT*S9=Dc_1PawmevT~O@ZT+jEwChz5S+`^~xeaMG?jzDrh zUxxNp{w1ilp>ldyBX=|MYv9qg*z#0hnUM-CN2dbI83 zO`qu+&53d*Q`@c!5=aJ-vr<8SnlGvMs(_>;h#P^&9Bx7&{#MrJ%Ee zhfk_}B(6x$_L9frGkbeRca9(O{oXERc7mmRMk)1Ys@zr{rp>CazhH0@Y^`dvHZB`$ zpMwsjRftE4JlfQ_b}Zy*yM6>U&!g}PtuzSFJe;?fcfsR5Cbg=Tceb^Eq%CiQ#;b{1 zq8=KmWu{U!tTnd9z5<8G*-t&>c)f9m38^5s2mIu^eyXFah4aM{s|p7t53ry}8`vam z5$T&5ct|VCR8v}2;05cQz8&N|X_C`9$<;z{%j^#JYZoNR=?~Jiwsg&wd%iMX`Yv@eybVnZczlM>)2b?yZ2dkr-l-VN>Jch# zRST1YJ!Ik{@i$Lrxwg_6`vjS)I#V6j%E#L8$(>2rq?Y-yc{MuADQD99yOWG8*Nq2; z;?#puop=2BTDuA#qyh-?NmBe2VEQ!N_%oEn&r%LQ2lM_F)cHJrjpG++BELve_$8Xg zzoi-cGR*uHs^;IptY4*CevRt*4T|xb)XZ-ur2o{;pH9kJ8<0-aZ=SripuhX>j@hxYsmLur&$ACwUKG zJtZ$<67sJr!W6@=|9&z*!&{ z(!arv3NK4u-F#fIEd`8GI;;5?Z2KGP4WHDPzlF7aN5lDhWKVklhVN)5j?;cEGg~#< z_FYPkK$J*-_h8Kt^eJQ{BXqVSH&~{M>Z{n~wTz~R0NJ8aO}5wblH*5|o)DAVZ`SqO z3rdgt>mJ(+^a7m+bS-`{R~}YXm^|lY9nR@p{EJIwrYcaO0IOTqcvWz<%X_m}In2me1*PXMr$g`%i_K;xq?=q#dk}RFZyi0 zY9siHf)|i`zjg1%b@GMO5Jl@NY;@R{+Gt4CD)u5B1NKngebkHl9Kb%D9qF}g3{1tj zPwY6i#7eg|={z%6GOSE>S}R|mZliwI+ZmaE>MSHum3}DyoQK6r>X?|>9ukpRb_|JM zv};m6zmgh2J(Q0IVfB19h{mWPRIG;41eFa<4o7mIP1R~dhgw*4P&N_PRHv4Mf$OA#RZS&tO=({42q3?>?WfOX4e% z^@YA|mYU%IWsTguZbo*VW0wM{dGKj}y1do}$Hw;BpuMz4t(@EWPW4lb`Ht73DyKso zUfe}GTd-L2rdF?LpM?{L-kMcq{25gG(Njf0ttr#@;Tja8FqlA27@)D)Vb zrqVGg0CzNnE>dN5mnx?x)O7lznn52T)P1aG(HCkqeXEXQR&&@;a}nH+=0R$I9uHMy zonvfZ-52iLsivOVwr$(CZM&V~6jR%_ZQE13o!U01oZi0w+)wu=E7|+&$w{7_thJu! zhsGcQN zY*Wmf7l*2O>aJzWk&DS4+8SrnAKuL!;u6|otqW>rYpgG^lMWgyQ#T(J|5QlXUKKnlqGn5iUWLOLf)fPXW( z$E9)gB6V$B(@crB4U!ph26az|`uWp9Bg$MSvl zb=K!<&&`9xwr^UP{89^65tG`leY|A6{HtlvE;ghyN%fHZdf+l%lsZ9DeWcYkJcJ~f zX3?Vm_30#q4w5TirhnjbPn4}3oW*grTWRTU=!~$d19Re zFJ7}ELMn13tz?CoTCd#F`>@8+!c3*9k$ibI*;;xF2I$Ja}{_D9v2JLBQDNN6|?0} z_07SQ4l8W5bXG88*^y3UBS|~*DRxVA6(fWc+6)mFr}gFU4_u=L-mE_xCgBK}%S^$i zZDa1j$`^?dwaAlMPB)1q6YtD?a>QPkd;G_~hbrekc~%NVrj;BG?5*B9ruxsx!CKXq zy{VSKz5}Bq9s5{RS~RW-t(20pTA){S?(zKsgqI9h3xWsi7Mewg6GaL;>>x_a*sytgQQ>kQ=%w?b|#Cs4FT0l7;GDL$kveJRWLy z+Nv-LZ5oWpOpab>Up+gKHoAEv>e!Klaz$EPrc>cG%qNIMYvCxmV*0JVruQ~XN_COL zT~A`=7<(^jqzJM?VbVqYxole|D|p*&I%Hjoxu4d*j$xXcR7XvyiZjBwhEy7s^WYt= zO@LRTAaHO8YQntX0$yjFCWs&S z6Drt?Bdk`>V}se$5*oOU ztkYj62d%s*(icr9(j72Q5N}mbMS>#+&Q-5+9gsO?#C7$?B7a}yaJbLd?JR3INrBe3 z>4NjqMneBgu(~m&9WZEL{Qe^1INR73(KBHw(O(h%QXFvb%OUQ>f5vvAWl^ZV+2w1( zLWV4F6)g)B#+$m&<}nO*gr0B8d5f-kpTO*1sYgD|1B_m4_4(w>$fx?Clw2QdWNggc zGX?7>?wU3Ym0abV!CCKYY+PiH6ac$=Usdq)3f-$h2dq6E+(8j>B?(@(`-PsXPff8q z=Y*PY{b+$h1o4Ovl1yget-EJMa=vP&<`f^gZ{$@l&02g|#3%DRPD;R7bKvP4?JL41 zl#Lf9$c5>F@ge3L(L&uNqis4PpXuD-p6&g>WxS%TTNxV_QoW?#d#c!Wsf3YmcEVIv z%HXdU7JUAk<~yT#-DNa8hj3ESTm|h1)DOV|v9fL&;);K5l9@_jnfQfMxZ%E9P@=L9hUtN{^KiXtc|M4lpo~oBnQ6N66&m zE79p&Ck1@5c`2qJ9TG4;^yJIO@*M+~KD8edb}0pLuOo`FiL3WE5}5C6#IB)tc=fXB z7=^{OM+E9l!obmf=H7`u@w!kr~V_i#= zJVf1@c>8g$IMo6d0MWmauI&on540}OgIs8WRBTMIKs5J!B4ItVvT)b%#}w%alAW(i ztq~RK{!9cbYEbu>qbVoeYH8p_7Ev=PA0CDCj_sf{sN)BP#tSM_<&Z)v-G|FIzLRj6 zS-}~=N+)_%^sB_=lYW!a=8cX=>C`SN@)yZCm=Oc>3yd^+0NOIYXrnhOc-c2vyc2t? zyif%xc`T%7WB(H7NQrZ<{KIslSPY%Y6I_{dB-y^ZCPI>+(N8Bs6Aw3_!0yaWnK?@8 zj@;Ac%t$8O53h(Juam%M=hr1M{UZ;{2#9RH72X94)oG~Ts@rqNj>uP38;6y+yVxMkE)i}IW zu3`7oVz=ROAFEpX{Uq5rY;o`GZ~aoCMxzeDoKaX^!;D4F;pRn*q=0GxhwEc{k382C zYV-UpXoldlGBmNFEhV0mM=^(S=~>`th>7IvrtaVf4e-hDr;z|}Kiu4vEG6wluSK@F zut(chAQ!1d3evDhm8Nbpb}a9w3|2^@)pN~)bJJj_)f2EqZ*~c`Jy*YXoRYeAqFKVQ zdPuRf(GWJf4D$9Tz1_h}AULdYtp`!OuvUOyz9xy(PyJ{iRDd+u`+-l->^bsV$4EZ* zPl@}UB@hyFdGi86a*y&yO!hDY0h95<)wRk*$_3-QB~D8CPm(qc8TjM|187`msUOjf zE%UQWl@JmBNT?D${wMjO#Na7-VHwZiCvb&f7IYb>2U&NR_K#{ocbL%M}bc zXJ%^V=Ln^UL+iKVzNu6a`LFl1d3fcfS#ki00yNUn`ncjd^ZX@oS>k*1;HB|FQ$zp6 z^Up|uiQJ&Gj6=@@a;}f%G)8oePl!l|9fkoba^hY3H1+0#WNHb9>C31_2dFb5o5=Ce zXuw?O+n+?2iMS~6&a!OM#F$YPasc8)l86%`-bVGl8vWXgigbssV!e)t=uZw?hlsj$ zbu_t(8vfR3D~=D&r1e46u*Xeyf;CyHhii#@1w|V<*Tp&dC&ji|XGMR-#`%m?`{GuIrs|bpyc!>gTpxSOAUG zkNB531D-IZsTrd+lS_1(THc7_*zI~osK!r&V^g_P10D;@)G>~T8^HE>c$C@ik(Ju_GqiqF3uk6W(ca3>1MNc8Mk9k!HL<2EbLOi_u1- zd4?gzHKvT_>@x1toSEN33ft2pm(x>HO4|cs9Caw`S8H#o&UtMkT zD27T94{Kv=d`XL<=7{^MTSPJ4G(UHZI_PL|82PXg<(c-6iOhXEfJY9#IbNsRFx6vN z^o;&3Uw&ZBoX9<;m@b)CXsg{Asa3t2j-#njBAodS98;x(SAs{w52fT)l6drq>B&T= zS)BwutPySq`K?>wJSsywdjpj#sjDqx4d$U^;t~TxrSnI(=QscG;I&Qdpq7zY_2V+t zP`>0U(5=Lj7WIQOK*_->eU!wmy9+EF=74awbZnlzEH$QxPBIat#2txQ98}~RMM@+K zW3z&AhF$n?^mzHk4LqyqsMz*~XJogv_5vhAMXK@5Jiiq)1HXZr6;iby{g;bF#*~3Y zZD~Z*Z2LJ#{HLZGy5K1J&N+ zTkuxTNRc|{1+HJx)vH_i7qn0HlC7%c7uNi&tuIQWqoK+gQt8i%75D{r{UQvj&`jD4 zn^If&1@ZV;8Olh-Og-S2G1mt?mp`<4GyVcrtCQob%NR_n+O`Wfqzub{AC z(WOy(x~79?(87}|Vo|%V-FXkO)IN0OqvU4nek@ag0-TldMO){k+9P!M)Uf)JSw6#s z&R`;@ZzPn|{=7aZtc8$6IC7e$-3&isPv83s-rjb2l>GV5=c;pdkU|d8 zc+JAI7pSy%C@WFh76w|x+A(!D-32e%vWFgKHc#8`9K9uH-zO~HkOD-6g zft8B4`w@KXv%ii~CQ6_CK1`Q+Z(o;gMq4lIbLl#FKN!v4Y3hgl7^?PQ>ZMRXBt#Qy#<=|Jq9LcPx z0(u7jb`1j<4B}LNOkx}yyupBTJ%4w;Lws|6b8G?MEM#_R+#sCVe`_m?^=(h#XK7;H zP8Dct3N1mvn-kghAMK~_5t(>Gy^{*&6}cE4777X~X&}%T@w>CtY8&Ur7$_ytit8a*8KOBHf3^8aCfeN>KAS4?^p%Fr{La30JbCez}^f88* zqk7pcz2v!Y5wDV(P^7(0r!8HOr??x%NEW- zeuX_6*FvAzeDEYh+M*0+wJu=YMYVCYJ<5RGUZpTPYjc8NFZyOj2r*AAyWQr+@l49`+@k6( zv~7E9i(fQ;VaD&^dXqCMfd+-7fFP9}vzt|Mlb((+PZQu$u2Wf@y{6qEYE!IWwdq>! zj_0erE|Y5YcEYme+oZ!InQ9X|kG7WRn*7A7SKTeSVqLr7wuaQP#T3kc&Kb;qtSOS~ zVOzw&seJ-D`uHF=8vQ^vir+Ej#MAS4J{1q@#C%l8HF6(s?L15LbC);0=S)}pEz8X{ z(}isF$`&Z#$<91IpQhYAl%3GQU0E1>m45Bnl8!yoIi=#05}0Mk zFMZs6i1H)(P~4W8Ib&FGd9``y`Ulya*9$Gzs{k&5wwB|bMvF&2p2&PACM*n<_F{QDduXtC49k={Zja$caDi=a z8*oC9s2kidi-G8ZFd=u<94>XF@yZ*c+89x4sN0^>@xnQD!Qf)9K5m|I=J6a`m|lW8 zEd}!SmY;4#Hb855XNTx;2d&T_>XP<1r1tZov3!_LqC@3*BgVl;HQfKn-2; z-tFhpCXY$N76k!OQ#Gt`%10Y4^HZ&4K=S1NbjEyBoZg*pUEjb9<7P{lfJe z47Y17IX<+}G>-F~2cw_G)k^b=-+!FxtqmLOWjaoqT8k`peSe5V;^+aQlqaC2E0guk z@;F3Ri>cTl&Ox0l)bCAEM3w^GX^|m@T zG)g_HT}sB=b6{NhI~#l$`ix&9;o1B9{Re?yxDOQF9T!2q_1KU3EOh3Cc3IlX6z8cXlPxc46}EdYwGri7$)Y(IJm@D%9JOUf#Q@MKxPJ6ufSdKe{4+vte>gKxVUI zfHUxd^!=R?e*7xt;w5wTTxZiK@z55-tP;tJK{vEZ+~Lj9E&FqoN~L_P zfV=WR;t@MMH1!j884B~v?caWlOTp|S;10FxRjBc>dJnzHvL3(3PxF{?_^>mxiR{nF zyGA>R3w}pl^aTa9t^O(5m3*r@Kh`i55h(Ydb$^=O>k~bBF3qS8(0F^s5Yj|Wdarad zdI0o}E&6D1dlj%ah5@6l$9?-)-yyOOt^82b*dTmN~$C!_m2VnKxE z1?#jxg-ZdNAK7NhV!o`|0z+vs0ir&^zRR#d7QU>W%QgLtkd7&Ge^+VsZW$GX3ye9KGOC)Ggw4Qke%q(H zER9+dEGf5g3(nAw{S1%}uylD*`pCIp;&zJ*By%eV&6odRrH)z>P?t$=*z&0f* zFMT1*3#H|Yxq3Vf+blI+jTM|xi{DMPy4CcPpynrXhqu+4BR*1kPBM%E=N|@?;&c&a zksu_%6ypr>m+C=Gq7||ND0&I;Oon))*O#M>v|`6N&mHoMrD_HLN|Y1LZH(Ynkkf{L znb{s?f9TwSu^prfy*OPs>dDVH273zsu)U{pf~PM_aAQ?o$AI$f;7Kpqnz zdFE0W>jkNX(9B*&zwA}sFE7U0zW;Vj;0n30^q8Uqs-Z%Z3!kWo5T7GbS_mMlP$B`~ z7mZGOw|5Yzi}GsXeE8ulx^iI2RM>FePsgSwVavN={%Ps0i^L$7{~#Y~-1Rsh*ghP$ zJYh+aO7w+ZGSmEvq+8@X`Aj2LTuJwop1~Lcx;=7(X}{ae41!t zNrIHiC}xH>a_Sf-Wu^Xwm?S#@nvVhWO{{it_4@=#gC$tX5(IqEu`aV}=W>mZH2E$+# zlyhdnn~ZhaXS^^DlOy~2HER;+1f(zJq`h@+DQu|G;#h92{axX`l>A-@)dhgsQfbaD zh%Qj_dC}uSOr3BUgYY!IBuBks7;fe2u=!ju9<^06i|pcY`fO?zvs75AbUk-0Nx930 z3B{lMxs#x>8PsM^vDzSQG1)T*Mpwnpoy)E!!*9e`E(qyxef8wlN-AvIRXu-%XF|re z@dpi+Hwj~9p}>?aPivgcQ#EOJf|MK!H)WK|&S0`~dS4Yrs4a*bn^{MJXoo$deDB=BBi8R{BgD^Od7CvzvE#FKK21!Kb|H>64<$ zj{jmd5R`^);{7Y9AeE??^#c-U7+EgbQbz6*LgtiILOP2*?aG$+gvuV@{B7s1jMotN zSaze6m&j=#trz!H`pj&q+<8jglXZ1K6VGc9Yhx8 zUJFi_K9MXzrp^P*sq=|nWlE4U9N3N;cB^!%1@k^I$(*$ON7tW^^v^1}muC2N2thCtJtz5NqR_bJaObf6W z36say)a-10_Du8r&ARTY>3RN3kqhdW(zO#P$JofVrnG=`aup@EZX6KfDd)o4K!Pa) zZgLA`SlZZ>acMSk;ac1AxkN8H^%GHcu-1_%q9BN_SueBdVcW`ozdKvW^0vR^4$uOO zoj;~MiT^Ma^RF$VN6{gBYK!a|`iSIF;i4uLp}Ug_?`GV8yM6kF;G*>JRZ)jD?j*u# zszBEU-6A(GEST6XG9-p}ZeRFzEvCd!My3uBBh&%E8lKu!Bu&yJuG5fTr*y0eYXRq@ z44HwbHGF_(rWpwE+~~th`mSr&FDml&>P3}ju|H5EjvIC;@=MZ}SK^fQY3hfAD+~Wn z)j%j_w9`yK6k*)*NAY@Pn-c_uyVD|w0LSU@Y}M3G+j_NhA?#xM0c$0|m)82HqY4Ue zmt|taD)ihH^`@jxQ~Qjy${uJM?H~@cO6F>2r?YV-x&5JL$8oId9kGTZ%``ef7O9(- zgXF!ej99@q9G(8nqIGK^NQ-kJc=ekBLJZtwzpj3qG;&sabq;D9R5=CT!N7a}SqrAK z38>IAs^wOVpvR!X1uGPCfMb)KR$2kT&AaXoYmbovL!MRj>ng@^>eVbW)gyR-PH4** zy5wZW8H}bbA@z%~)1S^oloa`A+|oruf-!+mTw|9PV;U_A$0|F3JBtBROsOhT9Hn_T zF9z8&x&5>hgVwn)=g+*1_pE<2eHBs^o>5RhY+z4;|8bS&akjCLvCa5RiV+o1#gD2$ zOWg-)>$)q<%;h=KM{Zm4ATs3WkNG1>-Tb;nCN^;ZUH4!>9eZk43vAtysTLX9okE`d zZ_^o{>fR_E`0UWaUI1Rd8W9^iwHYb&VcVMcVbQSQkLn>QZ^A7@Jn5OH;b?cV9zIM2 z@&Y6bdNfS*0L3QrWU|8_1J?5Z-a}#gP>Yg=D78mb&PD z1(Nbz^bA2P{jDT6U*97xw#Ji@y^#!6MPkLykixxKwUr(HvS{XySHjYPvo)3i)<803 z%{=TeL}z((8-kwQ4z^9136Da3NDYf4pFQCL${g&}aG`TgWuyQBhlnBdVF3T;P zf(EeJfQ3J`Wjst#{77C+%8&0(DS4;rKMkVz2F<*&nEYMaSTp6+%MsSzWp|k<^o$z+ z70V|@sgw&Hj�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 diff --git a/src/com/maxprograms/converters/Constants.java b/src/com/maxprograms/converters/Constants.java index 6944e322..41eb0549 100644 --- a/src/com/maxprograms/converters/Constants.java +++ b/src/com/maxprograms/converters/Constants.java @@ -19,7 +19,9 @@ private Constants() { public static final String TOOLID = "OpenXLIFF"; public static final String TOOLNAME = "OpenXLIFF Filters"; - public static final String VERSION = "1.3.3"; - public static final String BUILD = "20190716_1034"; + public static final String VERSION = "1.4.0"; + public static final String BUILD = "20190716_1852"; + public static final String SUCCESS = "0"; + public static final String ERROR = "1"; } diff --git a/src/com/maxprograms/converters/Convert.java b/src/com/maxprograms/converters/Convert.java index de4d20e3..08ceabd3 100644 --- a/src/com/maxprograms/converters/Convert.java +++ b/src/com/maxprograms/converters/Convert.java @@ -1,402 +1,401 @@ -/******************************************************************************* - * Copyright (c) 2003-2019 Maxprograms. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 1.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/org/documents/epl-v10.html - * - * Contributors: - * Maxprograms - initial API and implementation - *******************************************************************************/ -package com.maxprograms.converters; - -import com.maxprograms.converters.ditamap.DitaMap2Xliff; -import com.maxprograms.converters.html.Html2Xliff; -import com.maxprograms.converters.idml.Idml2Xliff; -import com.maxprograms.converters.javaproperties.Properties2Xliff; -import com.maxprograms.converters.javascript.Jscript2xliff; -import com.maxprograms.converters.mif.Mif2Xliff; -import com.maxprograms.converters.office.Office2Xliff; -import com.maxprograms.converters.plaintext.Text2Xliff; -import com.maxprograms.converters.po.Po2Xliff; -import com.maxprograms.converters.rc.Rc2Xliff; -import com.maxprograms.converters.resx.Resx2Xliff; -import com.maxprograms.converters.sdlxliff.Sdl2Xliff; -import com.maxprograms.converters.ts.Ts2Xliff; -import com.maxprograms.converters.txml.Txml2Xliff; -import com.maxprograms.converters.xml.Xml2Xliff; -import com.maxprograms.xliff2.ToXliff2; -import com.maxprograms.xml.Catalog; -import com.maxprograms.xml.Document; -import com.maxprograms.xml.Element; -import com.maxprograms.xml.Indenter; -import com.maxprograms.xml.SAXBuilder; -import com.maxprograms.xml.XMLNode; -import com.maxprograms.xml.XMLOutputter; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.lang.System.Logger; -import java.lang.System.Logger.Level; -import java.nio.charset.Charset; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.List; -import java.util.Set; -import java.util.SortedMap; -import java.util.Vector; -import javax.xml.parsers.ParserConfigurationException; -import org.xml.sax.SAXException; - - - - -public class Convert { - - private static final Logger LOGGER = System.getLogger(Convert.class.getName()); - - public static void main(String[] args) { - - String[] arguments = Utils.fixPath(args); - - String source = ""; - String type = ""; - String enc = ""; - String srcLang = ""; - String tgtLang = ""; - String srx = ""; - String skl = ""; - String xliff = ""; - String catalog = ""; - String ditaval = ""; - boolean embed = false; - boolean paragraph = false; - boolean xliff20 = false; - - for (int i = 0; i < arguments.length; i++) { - String arg = arguments[i]; - if (arg.equals("-version")) { - LOGGER.log(Level.INFO, () -> "Version: " + Constants.VERSION + " Build: " + Constants.BUILD); - return; - } - if (arg.equals("-help")) { - help(); - return; - } - if (arg.equals("-charsets")) { - listCharsets(); - return; - } - if (arg.equals("-file") && (i + 1) < arguments.length) { - source = arguments[i + 1]; - } - if (arg.equals("-type") && (i + 1) < arguments.length) { - type = arguments[i + 1]; - } - if (arg.equals("-enc") && (i + 1) < arguments.length) { - enc = arguments[i + 1]; - } - if (arg.equals("-srcLang") && (i + 1) < arguments.length) { - srcLang = arguments[i + 1]; - } - if (arg.equals("-tgtLang") && (i + 1) < arguments.length) { - tgtLang = arguments[i + 1]; - } - if (arg.equals("-srx") && (i + 1) < arguments.length) { - srx = arguments[i + 1]; - } - if (arg.equals("-skl") && (i + 1) < arguments.length) { - skl = arguments[i + 1]; - } - if (arg.equals("-xliff") && (i + 1) < arguments.length) { - xliff = arguments[i + 1]; - } - if (arg.equals("-catalog") && (i + 1) < arguments.length) { - catalog = arguments[i + 1]; - } - if (arg.equals("-ditaval") && (i + 1) < arguments.length) { - ditaval = arguments[i + 1]; - } - if (arg.equals("-embed")) { - embed = true; - } - if (arg.equals("-paragraph")) { - paragraph = true; - } - if (arg.equals("-2.0")) { - xliff20 = true; - } - } - if (arguments.length < 4) { - help(); - return; - } - if (source.isEmpty()) { - LOGGER.log(Level.ERROR, "Missing '-file' parameter."); - return; - } - File sourceFile = new File(source); - if (!sourceFile.exists()) { - LOGGER.log(Level.ERROR, "Source file does not exist."); - return; - } - if (type.isEmpty()) { - String detected = FileFormats.detectFormat(source); - if (detected != null) { - type = FileFormats.getShortName(detected); - LOGGER.log(Level.INFO, "Auto-detected type: " + type); - } else { - LOGGER.log(Level.ERROR, "Unable to auto-detect file format. Use '-type' parameter."); - return; - } - } - type = FileFormats.getFullName(type); - if (type == null) { - LOGGER.log(Level.ERROR, "Unknown file format."); - return; - } - if (enc.isEmpty()) { - Charset charset = EncodingResolver.getEncoding(source, type); - if (charset != null) { - enc = charset.name(); - LOGGER.log(Level.INFO, "Auto-detected encoding: " + enc); - } else { - LOGGER.log(Level.ERROR, "Unable to auto-detect character set. Use '-enc' parameter."); - return; - } - } - String[] encodings = EncodingResolver.getPageCodes(); - if (!Arrays.asList(encodings).contains(enc)) { - LOGGER.log(Level.ERROR, "Unsupported encoding."); - return; - } - if (srcLang.isEmpty()) { - LOGGER.log(Level.ERROR, "Missing '-srcLang' parameter."); - return; - } - try { - if (!Utils.isValidLanguage(srcLang)) { - LOGGER.log(Level.WARNING, "'" + srcLang + "' is not a valid language code."); - } - if (!tgtLang.isEmpty() && !Utils.isValidLanguage(tgtLang)) { - LOGGER.log(Level.WARNING, "'" + tgtLang + "' is not a valid language code."); - } - } catch (IOException e) { - LOGGER.log(Level.ERROR, "Error validating languages.", e); - return; - } - if (srx.isEmpty()) { - File srxFolder = new File(new File(System.getProperty("user.dir")), "srx"); - srx = new File(srxFolder, "default.srx").getAbsolutePath(); - } - File srxFile = new File(srx); - if (!srxFile.exists()) { - LOGGER.log(Level.ERROR, "SRX file does not exist."); - return; - } - if (catalog.isEmpty()) { - File catalogFolder = new File(new File(System.getProperty("user.dir")), "catalog"); - if (!catalogFolder.exists()) { - LOGGER.log(Level.ERROR, "'catalog' folder not found."); - return; - } - catalog = new File(catalogFolder, "catalog.xml").getAbsolutePath(); - } - File catalogFile = new File(catalog); - if (!catalogFile.exists()) { - LOGGER.log(Level.ERROR, "Catalog file does not exist."); - return; - } - if (skl.isEmpty()) { - skl = sourceFile.getAbsolutePath() + ".skl"; - } - if (xliff.isEmpty()) { - xliff = sourceFile.getAbsolutePath() + ".xlf"; - } - Hashtable params = new Hashtable<>(); - params.put("source", source); - params.put("xliff", xliff); - params.put("skeleton", skl); - params.put("format", type); - params.put("catalog", catalog); - params.put("srcEncoding", enc); - params.put("paragraph", paragraph ? "yes" : "no"); - params.put("srxFile", srx); - params.put("srcLang", srcLang); - if (!tgtLang.isEmpty()) { - params.put("tgtLang", tgtLang); - } - if (type.equals(FileFormats.getShortName(FileFormats.DITA)) && !ditaval.isEmpty()) { - params.put("ditaval", ditaval); - } - Vector result = run(params); - if ("0".equals(result.get(0))) { - if (embed) { - try { - addSkeleton(xliff, catalog); - } catch (SAXException | IOException | ParserConfigurationException e) { - LOGGER.log(Level.ERROR, "Error embedding skeleton.", e); - return; - } - } - if (xliff20) { - result = ToXliff2.run(new File(xliff), catalog); - if (!"0".equals(result.get(0))) { - LOGGER.log(Level.ERROR, result.get(1)); - } - } - } else { - LOGGER.log(Level.ERROR, result.get(1)); - } - } - - private static void help() { - String launcher = " convert.sh "; - if (System.getProperty("file.separator").equals("\\")) { - launcher = " convert.bat "; - } - String help = "Usage:\n\n" + launcher - + "[-help] [-version] -file sourceFile -srcLang sourceLang [-tgtLang targetLang] " - + "[-skl skeletonFile] [-xliff xliffFile] " + "[-type fileType] [-enc encoding] [-srx srxFile] " - + "[-catalog catalogFile] [-divatal ditaval] " + "[-embed] [-paragraph] [-2.0] [-charsets]\n\n" - + "Where:\n\n" - + " -help: (optional) Display this help information and exit\n" - + " -version: (optional) Display version & build information and exit\n" - + " -file: source file to convert\n" - + " -srgLang: source language code\n" - + " -tgtLang: (optional) target language code\n" - + " -xliff: (optional) XLIFF file to generate\n" - + " -skl: (optional) skeleton file to generate\n" - + " -type: (optional) document type\n" - + " -enc: (optional) character set code for the source file\n" - + " -srx: (optional) SRX file to use for segmentation\n" - + " -catalog: (optional) XML catalog to use for processing\n" - + " -ditaval: (optional) conditional processing file to use when converting DITA maps\n" - + " -embed: (optional) store skeleton inside the XLIFF file\n" - + " -paragraph: (optional) use paragraph segmentation\n" - + " -2.0: (optional) generate XLIFF 2.0\n" - + " -charsets: (optional) display a list of available character sets and exit\n\n" - + "Document Types\n\n" - + " INX = Adobe InDesign Interchange\n" - + " IDML = Adobe InDesign IDML\n" - + " DITA = DITA Map\n" - + " HTML = HTML Page\n" - + " JS = JavaScript\n" - + " JAVA = Java Properties\n" - + " MIF = MIF (Maker Interchange Format)\n" - + " OFF = Microsoft Office 2007 Document\n" - + " OO = OpenOffice Document\n" - + " PO = PO (Portable Objects)\n" - + " RC = RC (Windows C/C++ Resources)\n" - + " RESX = ResX (Windows .NET Resources)\n" - + " SDLXLIFF = SDLXLIFF Document\n" - + " TEXT = Plain Text\n" - + " TS = TS (Qt Linguist translation source)\n" - + " TXML = TXML Document\n" - + " XML = XML Document\n" - + " XMLG = XML (Generic)\n"; - System.out.println(help); - } - - public static void addSkeleton(String fileName, String catalog) - throws SAXException, IOException, ParserConfigurationException { - SAXBuilder builder = new SAXBuilder(); - builder.setEntityResolver(new Catalog(catalog)); - Document doc = builder.build(fileName); - Element root = doc.getRootElement(); - List files = root.getChildren("file"); - Iterator it = files.iterator(); - Set deleted = new HashSet<>(); - while (it.hasNext()) { - Element file = it.next(); - Element header = file.getChild("header"); - Element skl = header.getChild("skl"); - Element external = skl.getChild("external-file"); - String sklName = external.getAttributeValue("href"); - sklName = sklName.replaceAll("&", "&"); - sklName = sklName.replaceAll("<", "<"); - sklName = sklName.replaceAll(">", ">"); - sklName = sklName.replaceAll("'", "\'"); - sklName = sklName.replaceAll(""", "\""); - if (!deleted.contains(sklName)) { - File skeleton = new File(sklName); - Element internal = new Element("internal-file"); - internal.setAttribute("form", "base64"); - internal.addContent(Utils.encodeFromFile(skeleton.getAbsolutePath())); - skl.setContent(new Vector()); - skl.addContent(internal); - Files.delete(Paths.get(skeleton.toURI())); - deleted.add(sklName); - } - } - XMLOutputter outputter = new XMLOutputter(); - Indenter.indent(root, 2); - outputter.preserveSpace(true); - try (FileOutputStream out = new FileOutputStream(fileName)) { - outputter.output(doc, out); - } - } - - public static Vector run(Hashtable params) { - Vector result = new Vector<>(); - String format = params.get("format"); - if (format.equals(FileFormats.INX)) { - params.put("InDesign", "yes"); - result = Xml2Xliff.run(params); - } else if (format.equals(FileFormats.IDML)) { - result = Idml2Xliff.run(params); - } else if (format.equals(FileFormats.DITA)) { - result = DitaMap2Xliff.run(params); - } else if (format.equals(FileFormats.HTML)) { - File folder = new File(System.getProperty("user.dir"), "xmlfilter"); - params.put("iniFile", new File(folder, "init_html.xml").getAbsolutePath()); - result = Html2Xliff.run(params); - } else if (format.equals(FileFormats.JS)) { - result = Jscript2xliff.run(params); - } else if (format.equals(FileFormats.JAVA)) { - result = Properties2Xliff.run(params); - } else if (format.equals(FileFormats.MIF)) { - result = Mif2Xliff.run(params); - } else if (format.equals(FileFormats.OO) || format.equals(FileFormats.OFF)) { - result = Office2Xliff.run(params); - } else if (format.equals(FileFormats.PO)) { - result = Po2Xliff.run(params); - } else if (format.equals(FileFormats.RC)) { - result = Rc2Xliff.run(params); - } else if (format.equals(FileFormats.RESX)) { - result = Resx2Xliff.run(params); - } else if (format.equals(FileFormats.SDLXLIFF)) { - result = Sdl2Xliff.run(params); - } else if (format.equals(FileFormats.TEXT)) { - result = Text2Xliff.run(params); - } else if (format.equals(FileFormats.TS)) { - result = Ts2Xliff.run(params); - } else if (format.equals(FileFormats.TXML)) { - result = Txml2Xliff.run(params); - } else if (format.equals(FileFormats.XML)) { - result = Xml2Xliff.run(params); - } else if (format.equals(FileFormats.XMLG)) { - params.put("generic", "yes"); - result = Xml2Xliff.run(params); - } else { - result.add("1"); - result.add("Unknown file format."); - } - return result; - } - - private static void listCharsets() { - SortedMap available = Charset.availableCharsets(); - Set keySet = available.keySet(); - Iterator it = keySet.iterator(); - while (it.hasNext()) { - Charset charset = available.get(it.next()); - System.out.println(charset.displayName()); - } - } -} +/******************************************************************************* + * Copyright (c) 2003-2019 Maxprograms. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/org/documents/epl-v10.html + * + * Contributors: + * Maxprograms - initial API and implementation + *******************************************************************************/ +package com.maxprograms.converters; + +import com.maxprograms.converters.ditamap.DitaMap2Xliff; +import com.maxprograms.converters.html.Html2Xliff; +import com.maxprograms.converters.idml.Idml2Xliff; +import com.maxprograms.converters.javaproperties.Properties2Xliff; +import com.maxprograms.converters.javascript.Jscript2xliff; +import com.maxprograms.converters.mif.Mif2Xliff; +import com.maxprograms.converters.office.Office2Xliff; +import com.maxprograms.converters.plaintext.Text2Xliff; +import com.maxprograms.converters.po.Po2Xliff; +import com.maxprograms.converters.rc.Rc2Xliff; +import com.maxprograms.converters.resx.Resx2Xliff; +import com.maxprograms.converters.sdlxliff.Sdl2Xliff; +import com.maxprograms.converters.ts.Ts2Xliff; +import com.maxprograms.converters.txml.Txml2Xliff; +import com.maxprograms.converters.xml.Xml2Xliff; +import com.maxprograms.xliff2.ToXliff2; +import com.maxprograms.xml.Catalog; +import com.maxprograms.xml.Document; +import com.maxprograms.xml.Element; +import com.maxprograms.xml.Indenter; +import com.maxprograms.xml.SAXBuilder; +import com.maxprograms.xml.XMLNode; +import com.maxprograms.xml.XMLOutputter; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.lang.System.Logger; +import java.lang.System.Logger.Level; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.SortedMap; +import java.util.Vector; +import javax.xml.parsers.ParserConfigurationException; +import org.xml.sax.SAXException; + + + + +public class Convert { + + private static final Logger LOGGER = System.getLogger(Convert.class.getName()); + + public static void main(String[] args) { + + String[] arguments = Utils.fixPath(args); + + String source = ""; + String type = ""; + String enc = ""; + String srcLang = ""; + String tgtLang = ""; + String srx = ""; + String skl = ""; + String xliff = ""; + String catalog = ""; + String ditaval = ""; + boolean embed = false; + boolean paragraph = false; + boolean xliff20 = false; + + for (int i = 0; i < arguments.length; i++) { + String arg = arguments[i]; + if (arg.equals("-version")) { + LOGGER.log(Level.INFO, () -> "Version: " + Constants.VERSION + " Build: " + Constants.BUILD); + return; + } + if (arg.equals("-help")) { + help(); + return; + } + if (arg.equals("-charsets")) { + listCharsets(); + return; + } + if (arg.equals("-file") && (i + 1) < arguments.length) { + source = arguments[i + 1]; + } + if (arg.equals("-type") && (i + 1) < arguments.length) { + type = arguments[i + 1]; + } + if (arg.equals("-enc") && (i + 1) < arguments.length) { + enc = arguments[i + 1]; + } + if (arg.equals("-srcLang") && (i + 1) < arguments.length) { + srcLang = arguments[i + 1]; + } + if (arg.equals("-tgtLang") && (i + 1) < arguments.length) { + tgtLang = arguments[i + 1]; + } + if (arg.equals("-srx") && (i + 1) < arguments.length) { + srx = arguments[i + 1]; + } + if (arg.equals("-skl") && (i + 1) < arguments.length) { + skl = arguments[i + 1]; + } + if (arg.equals("-xliff") && (i + 1) < arguments.length) { + xliff = arguments[i + 1]; + } + if (arg.equals("-catalog") && (i + 1) < arguments.length) { + catalog = arguments[i + 1]; + } + if (arg.equals("-ditaval") && (i + 1) < arguments.length) { + ditaval = arguments[i + 1]; + } + if (arg.equals("-embed")) { + embed = true; + } + if (arg.equals("-paragraph")) { + paragraph = true; + } + if (arg.equals("-2.0")) { + xliff20 = true; + } + } + if (arguments.length < 4) { + help(); + return; + } + if (source.isEmpty()) { + LOGGER.log(Level.ERROR, "Missing '-file' parameter."); + return; + } + File sourceFile = new File(source); + if (!sourceFile.exists()) { + LOGGER.log(Level.ERROR, "Source file does not exist."); + return; + } + if (type.isEmpty()) { + String detected = FileFormats.detectFormat(source); + if (detected != null) { + type = FileFormats.getShortName(detected); + LOGGER.log(Level.INFO, "Auto-detected type: " + type); + } else { + LOGGER.log(Level.ERROR, "Unable to auto-detect file format. Use '-type' parameter."); + return; + } + } + type = FileFormats.getFullName(type); + if (type == null) { + LOGGER.log(Level.ERROR, "Unknown file format."); + return; + } + if (enc.isEmpty()) { + Charset charset = EncodingResolver.getEncoding(source, type); + if (charset != null) { + enc = charset.name(); + LOGGER.log(Level.INFO, "Auto-detected encoding: " + enc); + } else { + LOGGER.log(Level.ERROR, "Unable to auto-detect character set. Use '-enc' parameter."); + return; + } + } + String[] encodings = EncodingResolver.getPageCodes(); + if (!Arrays.asList(encodings).contains(enc)) { + LOGGER.log(Level.ERROR, "Unsupported encoding."); + return; + } + if (srcLang.isEmpty()) { + LOGGER.log(Level.ERROR, "Missing '-srcLang' parameter."); + return; + } + try { + if (!Utils.isValidLanguage(srcLang)) { + LOGGER.log(Level.WARNING, "'" + srcLang + "' is not a valid language code."); + } + if (!tgtLang.isEmpty() && !Utils.isValidLanguage(tgtLang)) { + LOGGER.log(Level.WARNING, "'" + tgtLang + "' is not a valid language code."); + } + } catch (IOException e) { + LOGGER.log(Level.ERROR, "Error validating languages.", e); + return; + } + if (srx.isEmpty()) { + File srxFolder = new File(new File(System.getProperty("user.dir")), "srx"); + srx = new File(srxFolder, "default.srx").getAbsolutePath(); + } + File srxFile = new File(srx); + if (!srxFile.exists()) { + LOGGER.log(Level.ERROR, "SRX file does not exist."); + return; + } + if (catalog.isEmpty()) { + File catalogFolder = new File(new File(System.getProperty("user.dir")), "catalog"); + if (!catalogFolder.exists()) { + LOGGER.log(Level.ERROR, "'catalog' folder not found."); + return; + } + catalog = new File(catalogFolder, "catalog.xml").getAbsolutePath(); + } + File catalogFile = new File(catalog); + if (!catalogFile.exists()) { + LOGGER.log(Level.ERROR, "Catalog file does not exist."); + return; + } + if (skl.isEmpty()) { + skl = sourceFile.getAbsolutePath() + ".skl"; + } + if (xliff.isEmpty()) { + xliff = sourceFile.getAbsolutePath() + ".xlf"; + } + Hashtable params = new Hashtable<>(); + params.put("source", source); + params.put("xliff", xliff); + params.put("skeleton", skl); + params.put("format", type); + params.put("catalog", catalog); + params.put("srcEncoding", enc); + params.put("paragraph", paragraph ? "yes" : "no"); + params.put("srxFile", srx); + params.put("srcLang", srcLang); + if (!tgtLang.isEmpty()) { + params.put("tgtLang", tgtLang); + } + if (type.equals(FileFormats.getShortName(FileFormats.DITA)) && !ditaval.isEmpty()) { + params.put("ditaval", ditaval); + } + Vector result = run(params); + if (embed && Constants.SUCCESS.equals(result.get(0))) { + addSkeleton(xliff, catalog); + } + if (xliff20 && Constants.SUCCESS.equals(result.get(0))) { + result = ToXliff2.run(new File(xliff), catalog); + } + if (!Constants.SUCCESS.equals(result.get(0))) { + LOGGER.log(Level.ERROR, "Conversion error", result.get(1)); + } + } + + private static void help() { + String launcher = " convert.sh "; + if (System.getProperty("file.separator").equals("\\")) { + launcher = " convert.bat "; + } + String help = "Usage:\n\n" + launcher + + "[-help] [-version] -file sourceFile -srcLang sourceLang [-tgtLang targetLang] " + + "[-skl skeletonFile] [-xliff xliffFile] " + "[-type fileType] [-enc encoding] [-srx srxFile] " + + "[-catalog catalogFile] [-divatal ditaval] " + "[-embed] [-paragraph] [-2.0] [-charsets]\n\n" + + "Where:\n\n" + + " -help: (optional) Display this help information and exit\n" + + " -version: (optional) Display version & build information and exit\n" + + " -file: source file to convert\n" + + " -srgLang: source language code\n" + + " -tgtLang: (optional) target language code\n" + + " -xliff: (optional) XLIFF file to generate\n" + + " -skl: (optional) skeleton file to generate\n" + + " -type: (optional) document type\n" + + " -enc: (optional) character set code for the source file\n" + + " -srx: (optional) SRX file to use for segmentation\n" + + " -catalog: (optional) XML catalog to use for processing\n" + + " -ditaval: (optional) conditional processing file to use when converting DITA maps\n" + + " -embed: (optional) store skeleton inside the XLIFF file\n" + + " -paragraph: (optional) use paragraph segmentation\n" + + " -2.0: (optional) generate XLIFF 2.0\n" + + " -charsets: (optional) display a list of available character sets and exit\n\n" + + "Document Types\n\n" + + " INX = Adobe InDesign Interchange\n" + + " IDML = Adobe InDesign IDML\n" + + " DITA = DITA Map\n" + + " HTML = HTML Page\n" + + " JS = JavaScript\n" + + " JAVA = Java Properties\n" + + " MIF = MIF (Maker Interchange Format)\n" + + " OFF = Microsoft Office 2007 Document\n" + + " OO = OpenOffice Document\n" + + " PO = PO (Portable Objects)\n" + + " RC = RC (Windows C/C++ Resources)\n" + + " RESX = ResX (Windows .NET Resources)\n" + + " SDLXLIFF = SDLXLIFF Document\n" + + " TEXT = Plain Text\n" + + " TS = TS (Qt Linguist translation source)\n" + + " TXML = TXML Document\n" + + " XML = XML Document\n" + + " XMLG = XML (Generic)\n"; + System.out.println(help); + } + + public static Vector addSkeleton(String fileName, String catalog) { + Vector result = new Vector<>(); + try { + SAXBuilder builder = new SAXBuilder(); + builder.setEntityResolver(new Catalog(catalog)); + Document doc = builder.build(fileName); + Element root = doc.getRootElement(); + List files = root.getChildren("file"); + Iterator it = files.iterator(); + Set deleted = new HashSet<>(); + while (it.hasNext()) { + Element file = it.next(); + Element header = file.getChild("header"); + Element skl = header.getChild("skl"); + Element external = skl.getChild("external-file"); + String sklName = external.getAttributeValue("href"); + sklName = sklName.replaceAll("&", "&"); + sklName = sklName.replaceAll("<", "<"); + sklName = sklName.replaceAll(">", ">"); + sklName = sklName.replaceAll("'", "\'"); + sklName = sklName.replaceAll(""", "\""); + if (!deleted.contains(sklName)) { + File skeleton = new File(sklName); + Element internal = new Element("internal-file"); + internal.setAttribute("form", "base64"); + internal.addContent(Utils.encodeFromFile(skeleton.getAbsolutePath())); + skl.setContent(new Vector()); + skl.addContent(internal); + Files.delete(Paths.get(skeleton.toURI())); + deleted.add(sklName); + } + } + XMLOutputter outputter = new XMLOutputter(); + Indenter.indent(root, 2); + outputter.preserveSpace(true); + try (FileOutputStream out = new FileOutputStream(fileName)) { + outputter.output(doc, out); + } + result.add(Constants.SUCCESS); + } catch (IOException | SAXException | ParserConfigurationException e) { + LOGGER.log(Level.ERROR, "Error adding skeleton", e); + result.add(Constants.ERROR); + result.add(e.getMessage()); + } + return result; + } + + public static Vector run(Hashtable params) { + Vector result = new Vector<>(); + String format = params.get("format"); + if (format.equals(FileFormats.INX)) { + params.put("InDesign", "yes"); + result = Xml2Xliff.run(params); + } else if (format.equals(FileFormats.IDML)) { + result = Idml2Xliff.run(params); + } else if (format.equals(FileFormats.DITA)) { + result = DitaMap2Xliff.run(params); + } else if (format.equals(FileFormats.HTML)) { + File folder = new File(System.getProperty("user.dir"), "xmlfilter"); + params.put("iniFile", new File(folder, "init_html.xml").getAbsolutePath()); + result = Html2Xliff.run(params); + } else if (format.equals(FileFormats.JS)) { + result = Jscript2xliff.run(params); + } else if (format.equals(FileFormats.JAVA)) { + result = Properties2Xliff.run(params); + } else if (format.equals(FileFormats.MIF)) { + result = Mif2Xliff.run(params); + } else if (format.equals(FileFormats.OO) || format.equals(FileFormats.OFF)) { + result = Office2Xliff.run(params); + } else if (format.equals(FileFormats.PO)) { + result = Po2Xliff.run(params); + } else if (format.equals(FileFormats.RC)) { + result = Rc2Xliff.run(params); + } else if (format.equals(FileFormats.RESX)) { + result = Resx2Xliff.run(params); + } else if (format.equals(FileFormats.SDLXLIFF)) { + result = Sdl2Xliff.run(params); + } else if (format.equals(FileFormats.TEXT)) { + result = Text2Xliff.run(params); + } else if (format.equals(FileFormats.TS)) { + result = Ts2Xliff.run(params); + } else if (format.equals(FileFormats.TXML)) { + result = Txml2Xliff.run(params); + } else if (format.equals(FileFormats.XML)) { + result = Xml2Xliff.run(params); + } else if (format.equals(FileFormats.XMLG)) { + params.put("generic", "yes"); + result = Xml2Xliff.run(params); + } else { + result.add(Constants.ERROR); + result.add("Unknown file format."); + } + return result; + } + + private static void listCharsets() { + SortedMap available = Charset.availableCharsets(); + Set keySet = available.keySet(); + Iterator it = keySet.iterator(); + while (it.hasNext()) { + Charset charset = available.get(it.next()); + System.out.println(charset.displayName()); + } + } +} diff --git a/src/com/maxprograms/converters/Merge.java b/src/com/maxprograms/converters/Merge.java index 49d4ebf5..7f546e93 100644 --- a/src/com/maxprograms/converters/Merge.java +++ b/src/com/maxprograms/converters/Merge.java @@ -1,569 +1,566 @@ -/******************************************************************************* - * Copyright (c) 2003-2019 Maxprograms. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 1.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/org/documents/epl-v10.html - * - * Contributors: - * Maxprograms - initial API and implementation - *******************************************************************************/ -package com.maxprograms.converters; - -import com.maxprograms.converters.ditamap.Xliff2DitaMap; -import com.maxprograms.converters.html.Xliff2Html; -import com.maxprograms.converters.idml.Xliff2Idml; -import com.maxprograms.converters.javaproperties.Xliff2Properties; -import com.maxprograms.converters.javascript.Xliff2jscript; -import com.maxprograms.converters.mif.Xliff2Mif; -import com.maxprograms.converters.office.Xliff2Office; -import com.maxprograms.converters.plaintext.Xliff2Text; -import com.maxprograms.converters.po.Xliff2Po; -import com.maxprograms.converters.rc.Xliff2Rc; -import com.maxprograms.converters.resx.Xliff2Resx; -import com.maxprograms.converters.sdlxliff.Xliff2Sdl; -import com.maxprograms.converters.ts.Xliff2Ts; -import com.maxprograms.converters.txml.Xliff2Txml; -import com.maxprograms.converters.xml.Xliff2Xml; -import com.maxprograms.xliff2.FromXliff2; -import com.maxprograms.xml.Catalog; -import com.maxprograms.xml.Document; -import com.maxprograms.xml.Element; -import com.maxprograms.xml.PI; -import com.maxprograms.xml.SAXBuilder; -import com.maxprograms.xml.XMLNode; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.lang.System.Logger; -import java.lang.System.Logger.Level; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.HashSet; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.List; -import java.util.TreeSet; -import java.util.Vector; -import javax.xml.parsers.ParserConfigurationException; -import org.xml.sax.SAXException; - - - - -public class Merge { - - private static final Logger LOGGER = System.getLogger(Merge.class.getName()); - - private static Vector segments; - protected static HashSet fileSet; - private static String dataType; - private static String encoding; - - private static Document doc; - private static Element root; - - - - public static void main(String[] args) { - String xliff = ""; - String target = ""; - String catalog = ""; - boolean unapproved = false; - boolean exportTMX = false; - - String[] arguments = Utils.fixPath(args); - for (int i = 0; i < arguments.length; i++) { - String arg = arguments[i]; - if (arg.equals("-version")) { - LOGGER.log(Level.INFO, () -> "Version: " + Constants.VERSION + " Build: " + Constants.BUILD); - return; - } - if (arg.equals("-help")) { - help(); - return; - } - if (arg.equals("-xliff") && (i + 1) < arguments.length) { - xliff = arguments[i + 1]; - } - if (arg.equals("-target") && (i + 1) < arguments.length) { - target = arguments[i + 1]; - } - if (arg.equals("-catalog") && (i + 1) < arguments.length) { - catalog = arguments[i + 1]; - } - if (arg.equals("-unapproved")) { - unapproved = true; - } - if (arg.equals("-export")) { - exportTMX = true; - } - } - if (arguments.length < 2) { - help(); - return; - } - if (xliff.isEmpty()) { - LOGGER.log(Level.ERROR, "Missing '-xliff' parameter."); - return; - } - if (target.isEmpty()) { - try { - target = getTargetFile(xliff); - } catch (IOException | SAXException | ParserConfigurationException e) { - LOGGER.log(Level.ERROR, "Error getting target file", e); - return; - } - } - if (target.isEmpty()) { - LOGGER.log(Level.ERROR, "Missing '-target' parameter."); - return; - } - if (catalog.isEmpty()) { - File catalogFolder = new File(new File(System.getProperty("user.dir")), "catalog"); - if (!catalogFolder.exists()) { - LOGGER.log(Level.ERROR, "'catalog' folder not found."); - return; - } - catalog = new File(catalogFolder, "catalog.xml").getAbsolutePath(); - } - File catalogFile = new File(catalog); - if (!catalogFile.exists()) { - LOGGER.log(Level.ERROR, "Catalog file does not exist."); - return; - } - - Vector result = merge(xliff, target, catalog, unapproved); - if (result.get(0).equals("0")) { - if (exportTMX) { - String tmx = ""; - if (xliff.toLowerCase().endsWith(".xlf")) { - tmx = xliff.substring(0, xliff.lastIndexOf('.')) + ".tmx"; - } else { - tmx = xliff + ".tmx"; - } - try { - TmxExporter.export(xliff, tmx, catalog); - } catch (SAXException | IOException | ParserConfigurationException e) { - LOGGER.log(Level.ERROR, "Error exporting TMX", e); - } - } - } - } - - public static Vector merge(String xliff, String target, String catalog, boolean acceptUnaproved) { - Vector result = new Vector<>(); - try { - loadXliff(xliff, catalog); - boolean unapproved = acceptUnaproved; - if (root.getAttributeValue("version").equals("2.0")) { - File tmpXliff = File.createTempFile("temp", ".xlf", new File(xliff).getParentFile()); - FromXliff2.run(xliff, tmpXliff.getAbsolutePath(), catalog); - loadXliff(tmpXliff.getAbsolutePath(), catalog); - Files.delete(Paths.get(tmpXliff.toURI())); - unapproved = true; - } - if (unapproved) { - approveAll(root); - } - - List files = root.getChildren("file"); - fileSet = new HashSet<>(); - Iterator it = files.iterator(); - while (it.hasNext()) { - Element file = it.next(); - fileSet.add(file.getAttributeValue("original")); - } - List encList = root.getPI("encoding"); - if (encList.isEmpty()) { - encList = files.get(0).getPI("encoding"); - } - if (!encList.isEmpty()) { - encoding = encList.get(0).getData(); - } - if (encoding == null) { - throw new IOException("Unknown character set"); - } - segments = new Vector<>(); - createList(root); - - if (fileSet.size() != 1) { - File f = new File(target); - if (f.exists()) { - if (!f.isDirectory()) { - LOGGER.log(Level.ERROR, () -> "'" + f.getAbsolutePath() + "' is not a directory"); - result.add("1"); - result.add("'" + f.getAbsolutePath() + "' is not a directory"); - return result; - } - } else { - f.mkdirs(); - } - } - Iterator ft = fileSet.iterator(); - Vector> paramsList = new Vector<>(); - while (ft.hasNext()) { - String file = ft.next(); - File xliffFile = File.createTempFile("temp", ".xlf"); - saveXliff(file, xliffFile); - Hashtable params = new Hashtable<>(); - params.put("xliff", xliffFile.getAbsolutePath()); - if (fileSet.size() == 1) { - params.put("backfile", target); - } else { - params.put("backfile", Utils.getAbsolutePath(target, file)); - } - params.put("encoding", encoding); - params.put("catalog", catalog); - params.put("format", dataType); - paramsList.add(params); - } - for (int i = 0; i < paramsList.size(); i++) { - Vector res = run(paramsList.get(i)); - File f = new File(paramsList.get(i).get("xliff")); - Files.deleteIfExists(Paths.get(f.toURI())); - if (!"0".equals(res.get(0))) { - LOGGER.log(Level.ERROR, res.get(1)); - return res; - } - } - result.add("0"); - } catch (IOException | SAXException | ParserConfigurationException ex) { - LOGGER.log(Level.ERROR, ex.getMessage(), ex); - result.add("1"); - result.add(ex.getMessage()); - } - return result; - } - - private static void help() { - String launcher = " merge.sh "; - if (System.getProperty("file.separator").equals("\\")) { - launcher = " merge.bat "; - } - String help = "Usage:\n\n" + launcher + "[-help] [-version] -xliff xliffFile -target targetFile " - + "[-catalog catalogFile] [-unapproved] [-export]\n\n" + "Where:\n\n" - + " -help: (optional) Display this help information and exit\n" - + " -version: (optional) Display version & build information and exit\n" - + " -xliff: XLIFF file to merge\n" - + " -target: (optional) translated file or folder where to store translated files\n" - + " -catalog: (optional) XML catalog to use for processing\n" - + " -unapproved: (optional) accept translations from unapproved segments\n" - + " -export: (optional) generate TMX file from approved segments"; - System.out.println(help); - } - - private static void approveAll(Element e) { - if (e.getName().equals("trans-unit")) { - Element target = e.getChild("target"); - if (target != null) { - e.setAttribute("approved", "yes"); - } - return; - } - List children = e.getChildren(); - for (int i = 0; i < children.size(); i++) { - approveAll(children.get(i)); - } - } - - protected static void loadXliff(String fileName, String catalog) - throws SAXException, IOException, ParserConfigurationException { - SAXBuilder builder = new SAXBuilder(); - builder.setEntityResolver(new Catalog(catalog)); - doc = builder.build(fileName); - root = doc.getRootElement(); - if (!root.getName().equals("xliff")) { - throw new IOException("Selected file is not an XLIFF document."); - } - } - - private static void createList(Element e) { - List children = e.getChildren(); - Iterator it = children.iterator(); - while (it.hasNext()) { - Element child = it.next(); - if (child.getName().equals("trans-unit")) { - child.removeChild("alt-trans"); - segments.add(child); - } else { - createList(child); - } - } - } - - private static void saveXliff(String fileName, File xliff) throws IOException { - try (FileOutputStream out = new FileOutputStream(xliff)) { - writeStr(out, "\n"); - List files = root.getChildren("file"); - Iterator it = files.iterator(); - List pis = null; - while (it.hasNext()) { - Element file = it.next(); - if (file.getAttributeValue("original").equals(fileName)) { - if (pis == null) { - pis = file.getPI(); - Iterator pt = pis.iterator(); - while (pt.hasNext()) { - PI pi = pt.next(); - if (pi.getTarget().equals("encoding")) { - encoding = pi.getData(); - } - writeStr(out, pis.get(0).toString()); - } - dataType = file.getAttributeValue("datatype"); - } - file.writeBytes(out, doc.getEncoding()); - } - } - writeStr(out, "\n"); - } - } - - private static void writeStr(FileOutputStream out, String string) throws IOException { - out.write(string.getBytes(StandardCharsets.UTF_8)); - } - - private static Vector run(Hashtable params) { - Vector result = new Vector<>(); - File temporary = null; - try { - dataType = params.get("format"); - loadXliff(params.get("xliff"), params.get("catalog")); - String skl = getSkeleton(); - params.put("skeleton", skl); - if (checkGroups(root)) { - temporary = File.createTempFile("group", ".xlf"); - removeGroups(root, doc); - try (FileOutputStream out = new FileOutputStream(temporary.getAbsolutePath())) { - doc.writeBytes(out, doc.getEncoding()); - } - params.put("xliff", temporary.getAbsolutePath()); - } - - if (dataType.equals(FileFormats.INX) || dataType.equals("x-inx")) { - params.put("InDesign", "yes"); - result = Xliff2Xml.run(params); - } else if (dataType.equals(FileFormats.IDML) || dataType.equals("x-idml")) { - result = Xliff2Idml.run(params); - } else if (dataType.equals(FileFormats.DITA) || dataType.equals("x-ditamap")) { - result = Xliff2DitaMap.run(params); - } else if (dataType.equals(FileFormats.HTML) || dataType.equals("html")) { - File folder = new File(System.getProperty("user.dir"), "xmlfilter"); - params.put("iniFile", new File(folder, "init_html.xml").getAbsolutePath()); - result = Xliff2Html.run(params); - } else if (dataType.equals(FileFormats.JS) || dataType.equals("javascript")) { - result = Xliff2jscript.run(params); - } else if (dataType.equals(FileFormats.JAVA) || dataType.equals("javapropertyresourcebundle") - || dataType.equals("javalistresourcebundle")) { - result = Xliff2Properties.run(params); - } else if (dataType.equals(FileFormats.MIF) || dataType.equals("mif")) { - result = Xliff2Mif.run(params); - } else if (dataType.equals(FileFormats.OFF) || dataType.equals("x-office")) { - result = Xliff2Office.run(params); - } else if (dataType.equals(FileFormats.PO) || dataType.equals("po")) { - result = Xliff2Po.run(params); - } else if (dataType.equals(FileFormats.RC) || dataType.equals("winres")) { - result = Xliff2Rc.run(params); - } else if (dataType.equals(FileFormats.RESX) || dataType.equals("resx")) { - result = Xliff2Resx.run(params); - } else if (dataType.equals(FileFormats.SDLXLIFF) || dataType.equals("x-sdlxliff")) { - result = Xliff2Sdl.run(params); - } else if (dataType.equals(FileFormats.TEXT) || dataType.equals("plaintext")) { - result = Xliff2Text.run(params); - } else if (dataType.equals(FileFormats.TS) || dataType.equals("x-ts")) { - result = Xliff2Ts.run(params); - } else if (dataType.equals(FileFormats.TXML) || dataType.equals("x-txml")) { - result = Xliff2Txml.run(params); - } else if (dataType.equals(FileFormats.XML) || dataType.equals("xml")) { - result = Xliff2Xml.run(params); - } else { - result.add("1"); - result.add("Unsupported XLIFF file."); - } - if (temporary != null) { - Files.delete(Paths.get(temporary.toURI())); - } - } catch (Exception e) { - LOGGER.log(Level.ERROR, "Error merging XLIFF", e); - result = new Vector<>(); - result.add("1"); - result.add(e.getMessage()); - } - return result; - } - - private static String getSkeleton() throws IOException { - String result = ""; - Element file = root.getChild("file"); - Element header = null; - if (file != null) { - dataType = file.getAttributeValue("datatype"); - header = file.getChild("header"); - if (header != null) { - Element mskl = header.getChild("skl"); - if (mskl != null) { - Element external = mskl.getChild("external-file"); - if (external != null) { - result = external.getAttributeValue("href"); - result = result.replaceAll("&", "&"); - result = result.replaceAll("<", "<"); - result = result.replaceAll(">", ">"); - result = result.replaceAll("'", "\'"); - result = result.replaceAll(""", "\""); - } else { - Element internal = mskl.getChild("internal-file"); - if (internal != null) { - File tmp = File.createTempFile("internal", ".skl"); - tmp.deleteOnExit(); - Utils.decodeToFile(internal.getText(), tmp.getAbsolutePath()); - return tmp.getAbsolutePath(); - } - return result; - } - } else { - return result; - } - } else { - return result; - } - } else { - return result; - } - - if (encoding != null && encoding.equals("")) { - List encList = root.getPI("encoding"); - if (!encList.isEmpty()) { - encoding = encList.get(0).getData(); - } - } - return result; - } - - private static boolean checkGroups(Element e) { - if (e.getName().equals("group") && e.getAttributeValue("ts", "").equals("hs-split")) { - return true; - } - List children = e.getChildren(); - Iterator i = children.iterator(); - while (i.hasNext()) { - Element child = i.next(); - if (checkGroups(child)) { - return true; - } - } - return false; - } - - public static void removeGroups(Element e, Document d) { - List children = e.getContent(); - for (int i = 0; i < children.size(); i++) { - XMLNode n = children.get(i); - if (n.getNodeType() == XMLNode.ELEMENT_NODE) { - Element child = (Element) n; - if (child.getName().equals("group") && child.getAttributeValue("ts", "").equals("hs-split")) { - child = joinGroup(child); - Element tu = new Element("trans-unit"); - tu.clone(child); - children.remove(i); - children.add(i, tu); - e.setContent(children); - } else { - removeGroups(child, d); - } - } - } - } - - private static Element joinGroup(Element child) { - List pair = child.getChildren(); - Element left = pair.get(0); - if (left.getName().equals("group")) { - left = joinGroup(left); - } - Element right = pair.get(1); - if (right.getName().equals("group")) { - right = joinGroup(right); - } - List srcContent = right.getChild("source").getContent(); - for (int k = 0; k < srcContent.size(); k++) { - XMLNode n = srcContent.get(k); - if (n.getNodeType() == XMLNode.ELEMENT_NODE) { - left.getChild("source").addContent(n); - } - if (n.getNodeType() == XMLNode.TEXT_NODE) { - left.getChild("source").addContent(n); - } - } - List tgtContent = right.getChild("target").getContent(); - for (int k = 0; k < tgtContent.size(); k++) { - XMLNode n = tgtContent.get(k); - if (n.getNodeType() == XMLNode.ELEMENT_NODE) { - left.getChild("target").addContent(n); - } - if (n.getNodeType() == XMLNode.TEXT_NODE) { - left.getChild("target").addContent(n); - } - } - left.setAttribute("id", child.getAttributeValue("id")); - if (left.getAttributeValue("approved").equalsIgnoreCase("yes") - && right.getAttributeValue("approved").equalsIgnoreCase("yes")) { - left.setAttribute("approved", "yes"); - } else { - left.setAttribute("approved", "no"); - } - return left; - } - - public static String getTargetFile(String file) throws IOException, SAXException, ParserConfigurationException { - SAXBuilder builder = new SAXBuilder(); - Element r = builder.build(file).getRootElement(); - if (!r.getName().equals("xliff")) { - throw new IOException("Selected file is not an XLIFF document"); - } - List files = r.getChildren("file"); - if (files.isEmpty()) { - throw new IOException("Selected file is not a valid XLIFF document"); - } - String version = r.getAttributeValue("version"); - String tgtLanguage = ""; - if (version.equals("1.2")) { - tgtLanguage = files.get(0).getAttributeValue("target-language"); - } else { - tgtLanguage = r.getAttributeValue("trgLang"); - } - if (tgtLanguage.isEmpty()) { - throw new IOException("Missing target language"); - } - String target = ""; - TreeSet originals = new TreeSet<>(); - Iterator it = files.iterator(); - while (it.hasNext()) { - originals.add(it.next().getAttributeValue("original","")); - } - if (originals.size() == 1) { - if (file.endsWith(".xlf")) { - target = file.substring(0,file.length()-4); - if (target.indexOf('.') != -1) { - target = target.substring(0,target.lastIndexOf('.')) - + "_" + tgtLanguage + target.substring(target.lastIndexOf(".")); - } - } else { - if (target.indexOf('.') != -1) { - target = target.substring(0,target.lastIndexOf('.')) - + "_" + tgtLanguage + target.substring(target.lastIndexOf('.')); - } - } - } else { - File p = new File(file).getParentFile(); - if (p == null) { - p = new File(System.getProperty("user.dir")); - } - target = new File(p, tgtLanguage).getAbsolutePath(); - } - return target; - } - -} +/******************************************************************************* + * Copyright (c) 2003-2019 Maxprograms. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/org/documents/epl-v10.html + * + * Contributors: + * Maxprograms - initial API and implementation + *******************************************************************************/ +package com.maxprograms.converters; + +import com.maxprograms.converters.ditamap.Xliff2DitaMap; +import com.maxprograms.converters.html.Xliff2Html; +import com.maxprograms.converters.idml.Xliff2Idml; +import com.maxprograms.converters.javaproperties.Xliff2Properties; +import com.maxprograms.converters.javascript.Xliff2jscript; +import com.maxprograms.converters.mif.Xliff2Mif; +import com.maxprograms.converters.office.Xliff2Office; +import com.maxprograms.converters.plaintext.Xliff2Text; +import com.maxprograms.converters.po.Xliff2Po; +import com.maxprograms.converters.rc.Xliff2Rc; +import com.maxprograms.converters.resx.Xliff2Resx; +import com.maxprograms.converters.sdlxliff.Xliff2Sdl; +import com.maxprograms.converters.ts.Xliff2Ts; +import com.maxprograms.converters.txml.Xliff2Txml; +import com.maxprograms.converters.xml.Xliff2Xml; +import com.maxprograms.xliff2.FromXliff2; +import com.maxprograms.xml.Catalog; +import com.maxprograms.xml.Document; +import com.maxprograms.xml.Element; +import com.maxprograms.xml.PI; +import com.maxprograms.xml.SAXBuilder; +import com.maxprograms.xml.XMLNode; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.lang.System.Logger; +import java.lang.System.Logger.Level; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.TreeSet; +import java.util.Vector; +import javax.xml.parsers.ParserConfigurationException; +import org.xml.sax.SAXException; + + + + +public class Merge { + + private static final Logger LOGGER = System.getLogger(Merge.class.getName()); + + private static Vector segments; + protected static HashSet fileSet; + private static String dataType; + private static String encoding; + + private static Document doc; + private static Element root; + + + + public static void main(String[] args) { + String xliff = ""; + String target = ""; + String catalog = ""; + boolean unapproved = false; + boolean exportTMX = false; + + String[] arguments = Utils.fixPath(args); + for (int i = 0; i < arguments.length; i++) { + String arg = arguments[i]; + if (arg.equals("-version")) { + LOGGER.log(Level.INFO, () -> "Version: " + Constants.VERSION + " Build: " + Constants.BUILD); + return; + } + if (arg.equals("-help")) { + help(); + return; + } + if (arg.equals("-xliff") && (i + 1) < arguments.length) { + xliff = arguments[i + 1]; + } + if (arg.equals("-target") && (i + 1) < arguments.length) { + target = arguments[i + 1]; + } + if (arg.equals("-catalog") && (i + 1) < arguments.length) { + catalog = arguments[i + 1]; + } + if (arg.equals("-unapproved")) { + unapproved = true; + } + if (arg.equals("-export")) { + exportTMX = true; + } + } + if (arguments.length < 2) { + help(); + return; + } + if (xliff.isEmpty()) { + LOGGER.log(Level.ERROR, "Missing '-xliff' parameter."); + return; + } + if (target.isEmpty()) { + try { + target = getTargetFile(xliff); + } catch (IOException | SAXException | ParserConfigurationException e) { + LOGGER.log(Level.ERROR, "Error getting target file", e); + return; + } + } + if (target.isEmpty()) { + LOGGER.log(Level.ERROR, "Missing '-target' parameter."); + return; + } + if (catalog.isEmpty()) { + File catalogFolder = new File(new File(System.getProperty("user.dir")), "catalog"); + if (!catalogFolder.exists()) { + LOGGER.log(Level.ERROR, "'catalog' folder not found."); + return; + } + catalog = new File(catalogFolder, "catalog.xml").getAbsolutePath(); + } + File catalogFile = new File(catalog); + if (!catalogFile.exists()) { + LOGGER.log(Level.ERROR, "Catalog file does not exist."); + return; + } + + Vector result = merge(xliff, target, catalog, unapproved); + if (exportTMX && Constants.SUCCESS.equals(result.get(0))) { + String tmx = ""; + if (xliff.toLowerCase().endsWith(".xlf")) { + tmx = xliff.substring(0, xliff.lastIndexOf('.')) + ".tmx"; + } else { + tmx = xliff + ".tmx"; + } + result = TmxExporter.export(xliff, tmx, catalog); + } + if (!Constants.SUCCESS.equals(result.get(0))) { + LOGGER.log(Level.ERROR, "Merge error: " + result.get(1)); + } + } + + public static Vector merge(String xliff, String target, String catalog, boolean acceptUnaproved) { + Vector result = new Vector<>(); + try { + loadXliff(xliff, catalog); + boolean unapproved = acceptUnaproved; + if (root.getAttributeValue("version").equals("2.0")) { + File tmpXliff = File.createTempFile("temp", ".xlf", new File(xliff).getParentFile()); + FromXliff2.run(xliff, tmpXliff.getAbsolutePath(), catalog); + loadXliff(tmpXliff.getAbsolutePath(), catalog); + Files.delete(Paths.get(tmpXliff.toURI())); + unapproved = true; + } + if (unapproved) { + approveAll(root); + } + + List files = root.getChildren("file"); + fileSet = new HashSet<>(); + Iterator it = files.iterator(); + while (it.hasNext()) { + Element file = it.next(); + fileSet.add(file.getAttributeValue("original")); + } + List encList = root.getPI("encoding"); + if (encList.isEmpty()) { + encList = files.get(0).getPI("encoding"); + } + if (!encList.isEmpty()) { + encoding = encList.get(0).getData(); + } + if (encoding == null) { + throw new IOException("Unknown character set"); + } + segments = new Vector<>(); + createList(root); + + if (fileSet.size() != 1) { + File f = new File(target); + if (f.exists()) { + if (!f.isDirectory()) { + LOGGER.log(Level.ERROR, () -> "'" + f.getAbsolutePath() + "' is not a directory"); + result.add(Constants.ERROR); + result.add("'" + f.getAbsolutePath() + "' is not a directory"); + return result; + } + } else { + f.mkdirs(); + } + } + Iterator ft = fileSet.iterator(); + Vector> paramsList = new Vector<>(); + while (ft.hasNext()) { + String file = ft.next(); + File xliffFile = File.createTempFile("temp", ".xlf"); + saveXliff(file, xliffFile); + Hashtable params = new Hashtable<>(); + params.put("xliff", xliffFile.getAbsolutePath()); + if (fileSet.size() == 1) { + params.put("backfile", target); + } else { + params.put("backfile", Utils.getAbsolutePath(target, file)); + } + params.put("encoding", encoding); + params.put("catalog", catalog); + params.put("format", dataType); + paramsList.add(params); + } + for (int i = 0; i < paramsList.size(); i++) { + Vector res = run(paramsList.get(i)); + File f = new File(paramsList.get(i).get("xliff")); + Files.deleteIfExists(Paths.get(f.toURI())); + if (!Constants.SUCCESS.equals(res.get(0))) { + LOGGER.log(Level.ERROR, res.get(1)); + return res; + } + } + result.add(Constants.SUCCESS); + } catch (IOException | SAXException | ParserConfigurationException ex) { + LOGGER.log(Level.ERROR, ex.getMessage(), ex); + result.add(Constants.ERROR); + result.add(ex.getMessage()); + } + return result; + } + + private static void help() { + String launcher = " merge.sh "; + if (System.getProperty("file.separator").equals("\\")) { + launcher = " merge.bat "; + } + String help = "Usage:\n\n" + launcher + "[-help] [-version] -xliff xliffFile -target targetFile " + + "[-catalog catalogFile] [-unapproved] [-export]\n\n" + "Where:\n\n" + + " -help: (optional) Display this help information and exit\n" + + " -version: (optional) Display version & build information and exit\n" + + " -xliff: XLIFF file to merge\n" + + " -target: (optional) translated file or folder where to store translated files\n" + + " -catalog: (optional) XML catalog to use for processing\n" + + " -unapproved: (optional) accept translations from unapproved segments\n" + + " -export: (optional) generate TMX file from approved segments"; + System.out.println(help); + } + + private static void approveAll(Element e) { + if (e.getName().equals("trans-unit")) { + Element target = e.getChild("target"); + if (target != null) { + e.setAttribute("approved", "yes"); + } + return; + } + List children = e.getChildren(); + for (int i = 0; i < children.size(); i++) { + approveAll(children.get(i)); + } + } + + protected static void loadXliff(String fileName, String catalog) + throws SAXException, IOException, ParserConfigurationException { + SAXBuilder builder = new SAXBuilder(); + builder.setEntityResolver(new Catalog(catalog)); + doc = builder.build(fileName); + root = doc.getRootElement(); + if (!root.getName().equals("xliff")) { + throw new IOException("Selected file is not an XLIFF document."); + } + } + + private static void createList(Element e) { + List children = e.getChildren(); + Iterator it = children.iterator(); + while (it.hasNext()) { + Element child = it.next(); + if (child.getName().equals("trans-unit")) { + child.removeChild("alt-trans"); + segments.add(child); + } else { + createList(child); + } + } + } + + private static void saveXliff(String fileName, File xliff) throws IOException { + try (FileOutputStream out = new FileOutputStream(xliff)) { + writeStr(out, "\n"); + List files = root.getChildren("file"); + Iterator it = files.iterator(); + List pis = null; + while (it.hasNext()) { + Element file = it.next(); + if (file.getAttributeValue("original").equals(fileName)) { + if (pis == null) { + pis = file.getPI(); + Iterator pt = pis.iterator(); + while (pt.hasNext()) { + PI pi = pt.next(); + if (pi.getTarget().equals("encoding")) { + encoding = pi.getData(); + } + writeStr(out, pis.get(0).toString()); + } + dataType = file.getAttributeValue("datatype"); + } + file.writeBytes(out, doc.getEncoding()); + } + } + writeStr(out, "\n"); + } + } + + private static void writeStr(FileOutputStream out, String string) throws IOException { + out.write(string.getBytes(StandardCharsets.UTF_8)); + } + + private static Vector run(Hashtable params) { + Vector result = new Vector<>(); + File temporary = null; + try { + dataType = params.get("format"); + loadXliff(params.get("xliff"), params.get("catalog")); + String skl = getSkeleton(); + params.put("skeleton", skl); + if (checkGroups(root)) { + temporary = File.createTempFile("group", ".xlf"); + removeGroups(root, doc); + try (FileOutputStream out = new FileOutputStream(temporary.getAbsolutePath())) { + doc.writeBytes(out, doc.getEncoding()); + } + params.put("xliff", temporary.getAbsolutePath()); + } + + if (dataType.equals(FileFormats.INX) || dataType.equals("x-inx")) { + params.put("InDesign", "yes"); + result = Xliff2Xml.run(params); + } else if (dataType.equals(FileFormats.IDML) || dataType.equals("x-idml")) { + result = Xliff2Idml.run(params); + } else if (dataType.equals(FileFormats.DITA) || dataType.equals("x-ditamap")) { + result = Xliff2DitaMap.run(params); + } else if (dataType.equals(FileFormats.HTML) || dataType.equals("html")) { + File folder = new File(System.getProperty("user.dir"), "xmlfilter"); + params.put("iniFile", new File(folder, "init_html.xml").getAbsolutePath()); + result = Xliff2Html.run(params); + } else if (dataType.equals(FileFormats.JS) || dataType.equals("javascript")) { + result = Xliff2jscript.run(params); + } else if (dataType.equals(FileFormats.JAVA) || dataType.equals("javapropertyresourcebundle") + || dataType.equals("javalistresourcebundle")) { + result = Xliff2Properties.run(params); + } else if (dataType.equals(FileFormats.MIF) || dataType.equals("mif")) { + result = Xliff2Mif.run(params); + } else if (dataType.equals(FileFormats.OFF) || dataType.equals("x-office")) { + result = Xliff2Office.run(params); + } else if (dataType.equals(FileFormats.PO) || dataType.equals("po")) { + result = Xliff2Po.run(params); + } else if (dataType.equals(FileFormats.RC) || dataType.equals("winres")) { + result = Xliff2Rc.run(params); + } else if (dataType.equals(FileFormats.RESX) || dataType.equals("resx")) { + result = Xliff2Resx.run(params); + } else if (dataType.equals(FileFormats.SDLXLIFF) || dataType.equals("x-sdlxliff")) { + result = Xliff2Sdl.run(params); + } else if (dataType.equals(FileFormats.TEXT) || dataType.equals("plaintext")) { + result = Xliff2Text.run(params); + } else if (dataType.equals(FileFormats.TS) || dataType.equals("x-ts")) { + result = Xliff2Ts.run(params); + } else if (dataType.equals(FileFormats.TXML) || dataType.equals("x-txml")) { + result = Xliff2Txml.run(params); + } else if (dataType.equals(FileFormats.XML) || dataType.equals("xml")) { + result = Xliff2Xml.run(params); + } else { + result.add(Constants.ERROR); + result.add("Unsupported XLIFF file."); + } + if (temporary != null) { + Files.delete(Paths.get(temporary.toURI())); + } + } catch (Exception e) { + LOGGER.log(Level.ERROR, "Error merging XLIFF", e); + result = new Vector<>(); + result.add(Constants.ERROR); + result.add(e.getMessage()); + } + return result; + } + + private static String getSkeleton() throws IOException { + String result = ""; + Element file = root.getChild("file"); + Element header = null; + if (file != null) { + dataType = file.getAttributeValue("datatype"); + header = file.getChild("header"); + if (header != null) { + Element mskl = header.getChild("skl"); + if (mskl != null) { + Element external = mskl.getChild("external-file"); + if (external != null) { + result = external.getAttributeValue("href"); + result = result.replaceAll("&", "&"); + result = result.replaceAll("<", "<"); + result = result.replaceAll(">", ">"); + result = result.replaceAll("'", "\'"); + result = result.replaceAll(""", "\""); + } else { + Element internal = mskl.getChild("internal-file"); + if (internal != null) { + File tmp = File.createTempFile("internal", ".skl"); + tmp.deleteOnExit(); + Utils.decodeToFile(internal.getText(), tmp.getAbsolutePath()); + return tmp.getAbsolutePath(); + } + return result; + } + } else { + return result; + } + } else { + return result; + } + } else { + return result; + } + + if (encoding != null && encoding.equals("")) { + List encList = root.getPI("encoding"); + if (!encList.isEmpty()) { + encoding = encList.get(0).getData(); + } + } + return result; + } + + private static boolean checkGroups(Element e) { + if (e.getName().equals("group") && e.getAttributeValue("ts", "").equals("hs-split")) { + return true; + } + List children = e.getChildren(); + Iterator i = children.iterator(); + while (i.hasNext()) { + Element child = i.next(); + if (checkGroups(child)) { + return true; + } + } + return false; + } + + public static void removeGroups(Element e, Document d) { + List children = e.getContent(); + for (int i = 0; i < children.size(); i++) { + XMLNode n = children.get(i); + if (n.getNodeType() == XMLNode.ELEMENT_NODE) { + Element child = (Element) n; + if (child.getName().equals("group") && child.getAttributeValue("ts", "").equals("hs-split")) { + child = joinGroup(child); + Element tu = new Element("trans-unit"); + tu.clone(child); + children.remove(i); + children.add(i, tu); + e.setContent(children); + } else { + removeGroups(child, d); + } + } + } + } + + private static Element joinGroup(Element child) { + List pair = child.getChildren(); + Element left = pair.get(0); + if (left.getName().equals("group")) { + left = joinGroup(left); + } + Element right = pair.get(1); + if (right.getName().equals("group")) { + right = joinGroup(right); + } + List srcContent = right.getChild("source").getContent(); + for (int k = 0; k < srcContent.size(); k++) { + XMLNode n = srcContent.get(k); + if (n.getNodeType() == XMLNode.ELEMENT_NODE) { + left.getChild("source").addContent(n); + } + if (n.getNodeType() == XMLNode.TEXT_NODE) { + left.getChild("source").addContent(n); + } + } + List tgtContent = right.getChild("target").getContent(); + for (int k = 0; k < tgtContent.size(); k++) { + XMLNode n = tgtContent.get(k); + if (n.getNodeType() == XMLNode.ELEMENT_NODE) { + left.getChild("target").addContent(n); + } + if (n.getNodeType() == XMLNode.TEXT_NODE) { + left.getChild("target").addContent(n); + } + } + left.setAttribute("id", child.getAttributeValue("id")); + if (left.getAttributeValue("approved").equalsIgnoreCase("yes") + && right.getAttributeValue("approved").equalsIgnoreCase("yes")) { + left.setAttribute("approved", "yes"); + } else { + left.setAttribute("approved", "no"); + } + return left; + } + + public static String getTargetFile(String file) throws IOException, SAXException, ParserConfigurationException { + SAXBuilder builder = new SAXBuilder(); + Element r = builder.build(file).getRootElement(); + if (!r.getName().equals("xliff")) { + throw new IOException("Selected file is not an XLIFF document"); + } + List files = r.getChildren("file"); + if (files.isEmpty()) { + throw new IOException("Selected file is not a valid XLIFF document"); + } + String version = r.getAttributeValue("version"); + String tgtLanguage = ""; + if (version.equals("1.2")) { + tgtLanguage = files.get(0).getAttributeValue("target-language"); + } else { + tgtLanguage = r.getAttributeValue("trgLang"); + } + if (tgtLanguage.isEmpty()) { + throw new IOException("Missing target language"); + } + String target = ""; + TreeSet originals = new TreeSet<>(); + Iterator it = files.iterator(); + while (it.hasNext()) { + originals.add(it.next().getAttributeValue("original","")); + } + if (originals.size() == 1) { + if (file.endsWith(".xlf")) { + target = file.substring(0,file.length()-4); + if (target.indexOf('.') != -1) { + target = target.substring(0,target.lastIndexOf('.')) + + "_" + tgtLanguage + target.substring(target.lastIndexOf(".")); + } + } else { + if (target.indexOf('.') != -1) { + target = target.substring(0,target.lastIndexOf('.')) + + "_" + tgtLanguage + target.substring(target.lastIndexOf('.')); + } + } + } else { + File p = new File(file).getParentFile(); + if (p == null) { + p = new File(System.getProperty("user.dir")); + } + target = new File(p, tgtLanguage).getAbsolutePath(); + } + return target; + } + +} diff --git a/src/com/maxprograms/converters/TmxExporter.java b/src/com/maxprograms/converters/TmxExporter.java index af6b06a5..914d32e1 100755 --- a/src/com/maxprograms/converters/TmxExporter.java +++ b/src/com/maxprograms/converters/TmxExporter.java @@ -1,455 +1,466 @@ -/******************************************************************************* - * Copyright (c) 2003-2019 Maxprograms. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 1.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/org/documents/epl-v10.html - * - * Contributors: - * Maxprograms - initial API and implementation - *******************************************************************************/ -package com.maxprograms.converters; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.Calendar; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.List; -import java.util.TimeZone; - -import javax.xml.parsers.ParserConfigurationException; - -import org.xml.sax.SAXException; - -import com.maxprograms.xliff2.FromXliff2; -import com.maxprograms.xml.Attribute; -import com.maxprograms.xml.Catalog; -import com.maxprograms.xml.Document; -import com.maxprograms.xml.Element; -import com.maxprograms.xml.PI; -import com.maxprograms.xml.SAXBuilder; -import com.maxprograms.xml.XMLNode; -import com.maxprograms.xml.XMLUtils; - -public class TmxExporter { - - public static final String DOUBLEPRIME = "\u2033"; - public static final String MATHLT = "\u2039"; - public static final String MATHGT = "\u200B\u203A"; - public static final String GAMP = "\u200B\u203A"; - - static Hashtable docProperties; - private static String sourceLang; - private static String targetLang; - private static String today; - private static int match; - private static String original; - private static int filenumbr; - - private TmxExporter() { - // do not instantiate this class - } - - public static void export(String xliff, String tmx, String catalog) - throws SAXException, IOException, ParserConfigurationException { - - today = getTmxDate(); - filenumbr = 0; - - SAXBuilder builder = new SAXBuilder(); - builder.setEntityResolver(new Catalog(catalog)); - Document doc = builder.build(xliff); - Element root = doc.getRootElement(); - if (root.getAttributeValue("version").equals("2.0")) { - File tmpXliff = File.createTempFile("temp", ".xlf", new File(xliff).getParentFile()); - FromXliff2.run(xliff, tmpXliff.getAbsolutePath(), catalog); - doc = builder.build(tmpXliff); - root = doc.getRootElement(); - Files.delete(Paths.get(tmpXliff.toURI())); - } - - try (FileOutputStream output = new FileOutputStream(tmx)) { - Element firstFile = root.getChild("file"); - - docProperties = new Hashtable<>(); - List slist = root.getPI("subject"); - if (!slist.isEmpty()) { - docProperties.put("subject", slist.get(0).getData()); - } else { - docProperties.put("subject", ""); - } - List plist = root.getPI("project"); - if (!plist.isEmpty()) { - docProperties.put("project", plist.get(0).getData()); - } else { - docProperties.put("project", ""); - } - List clist = root.getPI("customer"); - if (!clist.isEmpty()) { - docProperties.put("customer", clist.get(0).getData()); - } else { - docProperties.put("customer", ""); - } - - sourceLang = firstFile.getAttributeValue("source-language"); - - writeString(output, "\n"); - writeString(output, - "\n"); - writeString(output, "\n"); - writeString(output, - "

\n" + - "
\n"); - writeString(output, "\n"); - - List files = root.getChildren("file"); - Iterator fileiterator = files.iterator(); - while (fileiterator.hasNext()) { - Element file = fileiterator.next(); - sourceLang = file.getAttributeValue("source-language"); - targetLang = file.getAttributeValue("target-language"); - original = "" + file.getAttributeValue("original").hashCode(); - recurse(output, file); - filenumbr++; - } - writeString(output, "\n"); - writeString(output, ""); - } - } - - private static void recurse(FileOutputStream output, Element e) throws IOException { - List list = e.getChildren(); - Iterator i = list.iterator(); - while (i.hasNext()) { - Element element = i.next(); - if (element.getName().equals("trans-unit")) { - writeSegment(output, element); - } else { - recurse(output, element); - } - } - } - - private static void writeSegment(FileOutputStream output, Element segment) throws IOException { - - String id = original + "-" + filenumbr + "-" + segment.getAttributeValue("id").hashCode(); - - if (segment.getAttributeValue("approved").equals("yes")) { - Element source = segment.getChild("source"); - if (source.getContent().isEmpty()) { - // empty segment, nothing to export - return; - } - Element target = segment.getChild("target"); - if (target == null) { - return; - } - String srcLang = source.getAttributeValue("xml:lang", ""); - if (srcLang.isEmpty()) { - srcLang = sourceLang; - } - String tgtLang = target.getAttributeValue("xml:lang", ""); - if (tgtLang.isEmpty()) { - tgtLang = targetLang; - } - writeString(output, "\n"); - - String customer = docProperties.get("customer"); - String project = docProperties.get("project"); - String subject = docProperties.get("subject"); - - if (!subject.isEmpty()) { - writeString(output, "" + XMLUtils.cleanText(subject) + "\n"); - } - if (!project.isEmpty()) { - writeString(output, "" + XMLUtils.cleanText(project) + "\n"); - } - if (!customer.isEmpty()) { - writeString(output, "" + XMLUtils.cleanText(customer) + "\n"); - } - - List notes = segment.getChildren("note"); - Iterator it = notes.iterator(); - while (it.hasNext()) { - Element note = it.next(); - String lang = note.getAttributeValue("xml:lang", ""); - if (!lang.isEmpty()) { - lang = " xml:lang=\"" + lang + "\""; - } - writeString(output, "" + XMLUtils.cleanText(note.getText()) + "\n"); - } - String srcText = extractText(source); - String tgtText = extractText(target); - if (!segment.getAttributeValue("xml:space", "default").equals("preserve")) { - srcText = srcText.trim(); - tgtText = tgtText.trim(); - } - writeString(output, "\n" + srcText - + "\n\n"); - writeString(output, "\n" + tgtText - + "\n\n"); - writeString(output, "\n"); - } - } - - public static String extractText(Element src) { - - String type = src.getName(); - - if (type.equals("source") || type.equals("target")) { - match = 0; - List l = src.getContent(); - Iterator i = l.iterator(); - StringBuilder text = new StringBuilder(); - while (i.hasNext()) { - XMLNode o = i.next(); - switch (o.getNodeType()) { - case XMLNode.TEXT_NODE: - text.append(o.toString()); - break; - case XMLNode.ELEMENT_NODE: - Element e = (Element) o; - text.append(extractText(e)); - break; - default: - // ignore - } - } - return text.toString(); - } - - if (type.equals("bx") || type.equals("ex") || type.equals("ph")) { - List l = src.getContent(); - Iterator i = l.iterator(); - String ctype = src.getAttributeValue("ctype"); - if (!ctype.isEmpty()) { - ctype = " type=\"" + XMLUtils.cleanText(ctype) + "\""; - } - String assoc = src.getAttributeValue("assoc"); - if (!assoc.isEmpty()) { - assoc = " assoc=\"" + XMLUtils.cleanText(assoc) + "\""; - } - String id = ""; - if (type.equals("ph")) { - id = src.getAttributeValue("id"); - if (!id.isEmpty()) { - id = " x=\"" + XMLUtils.cleanText(id) + "\""; - } - } - StringBuilder text = new StringBuilder(); - text.append("'); - while (i.hasNext()) { - XMLNode o = i.next(); - switch (o.getNodeType()) { - case XMLNode.TEXT_NODE: - text.append(o.toString()); - break; - case XMLNode.ELEMENT_NODE: - Element e = (Element) o; - if (e.getName().equals("sub")) { - text.append(extractText(e)); - } - if (!e.getName().equals("mrk")) { - text.append(extractText(e)); - } - break; - default: - // ignore - } - } - return text + ""; - } - - if (type.equals("g") || type.equals("x")) { - String open = "<" + src.getName(); - List atts = src.getAttributes(); - Iterator h = atts.iterator(); - while (h.hasNext()) { - Attribute a = h.next(); - open = open + " " + a.getName() + "=\"" + a.getValue() + "\""; - } - List l = src.getContent(); - if (!l.isEmpty()) { - open = open + ">"; - int i = match; - match++; - String text = "" + XMLUtils.cleanText(open) - + ""; - Iterator k = l.iterator(); - while (k.hasNext()) { - XMLNode n = k.next(); - if (n.getNodeType() == XMLNode.TEXT_NODE) { - text = text + n.toString(); - } - if (n.getNodeType() == XMLNode.ELEMENT_NODE) { - text = text + extractText((Element) n); - } - } - String close = ""; - return text + "" + XMLUtils.cleanText(close) + ""; - } - return "" + XMLUtils.cleanText(open + "/>") + ""; - } - - if (type.equals("it")) { - List l = src.getContent(); - Iterator i = l.iterator(); - String ctype = src.getAttributeValue("ctype", ""); - if (!ctype.isEmpty()) { - ctype = " type=\"" + XMLUtils.cleanText(ctype) + "\""; - } - String pos = src.getAttributeValue("pos", ""); - if (pos.equals("open")) { - pos = " pos=\"begin\""; - } else if (pos.equals("close")) { - pos = " pos=\"end\""; - } - StringBuilder text = new StringBuilder(); - text.append("'); - while (i.hasNext()) { - XMLNode o = i.next(); - switch (o.getNodeType()) { - case XMLNode.TEXT_NODE: - text.append(o.toString()); - break; - case XMLNode.ELEMENT_NODE: - text.append(extractText((Element) o)); - break; - default: - // ignore - } - } - text.append(""); - return text.toString(); - } - - if (type.equals("bpt") || type.equals("ept")) { - List l = src.getContent(); - Iterator i = l.iterator(); - String ctype = src.getAttributeValue("ctype", ""); - if (!ctype.isEmpty()) { - ctype = " type=\"" + XMLUtils.cleanText(ctype) + "\""; - } - String rid = src.getAttributeValue("rid", ""); - if (!rid.isEmpty()) { - rid = " i=\"" + XMLUtils.cleanText(rid) + "\""; - } else { - rid = " i=\"" + XMLUtils.cleanText(src.getAttributeValue("id", "")) + "\""; - } - StringBuilder text = new StringBuilder(); - text.append('<'); - text.append(type); - text.append(ctype); - text.append(rid); - text.append('>'); - while (i.hasNext()) { - XMLNode o = i.next(); - switch (o.getNodeType()) { - case XMLNode.TEXT_NODE: - text.append( o.toString()); - break; - case XMLNode.ELEMENT_NODE: - text.append(extractText((Element) o)); - break; - default: - // ignore - } - } - text.append("'); - return text.toString(); - } - - if (type.equals("sub")) { - List l = src.getContent(); - Iterator i = l.iterator(); - StringBuilder text = new StringBuilder(); - text.append(""); - while (i.hasNext()) { - XMLNode o = i.next(); - switch (o.getNodeType()) { - case XMLNode.TEXT_NODE: - text.append(o.toString()); - break; - case XMLNode.ELEMENT_NODE: - Element e = (Element) o; - if (!e.getName().equals("mrk")) { - text.append(extractText(e)); - } - break; - default: - // ignore - } - } - text.append(""); - return text.toString(); - } - if (type.equals("mrk")) { - if (src.getAttributeValue("mtype", "").equals("term")) { - // ignore terminology entries - return XMLUtils.cleanText(src.getText()); - } - if (src.getAttributeValue("mtype", "").equals("protected")) { - String ts = src.getAttributeValue("ts", ""); - ts = restoreChars(ts).trim(); - String name = ""; - for (int i = 1; i < ts.length(); i++) { - if (Character.isSpaceChar(ts.charAt(i))) { - break; - } - name = name + ts.charAt(i); - } - return "" + XMLUtils.cleanText(ts) + "" + XMLUtils.cleanText(src.getText()) - + "" + XMLUtils.cleanText("") + ""; - } - return "" - + XMLUtils.cleanText(src.getText()) + ""; - } - return null; - } - - private static String restoreChars(String string) { - String result = string.replaceAll(MATHLT, "<"); - result = result.replaceAll(MATHGT, ">"); - result = result.replaceAll(DOUBLEPRIME, "\""); - return result.replaceAll(GAMP, "&"); - } - - private static void writeString(FileOutputStream output, String input) throws IOException { - output.write(input.getBytes(StandardCharsets.UTF_8)); - } - - public static String getTmxDate() { - Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT")); - String sec = (calendar.get(Calendar.SECOND) < 10 ? "0" : "") + calendar.get(Calendar.SECOND); - String min = (calendar.get(Calendar.MINUTE) < 10 ? "0" : "") + calendar.get(Calendar.MINUTE); - String hour = (calendar.get(Calendar.HOUR_OF_DAY) < 10 ? "0" : "") + calendar.get(Calendar.HOUR_OF_DAY); - String mday = (calendar.get(Calendar.DATE) < 10 ? "0" : "") + calendar.get(Calendar.DATE); - String mon = (calendar.get(Calendar.MONTH) < 9 ? "0" : "") + (calendar.get(Calendar.MONTH) + 1); - String longyear = "" + calendar.get(Calendar.YEAR); - - return longyear + mon + mday + "T" + hour + min + sec + "Z"; - } +/******************************************************************************* + * Copyright (c) 2003-2019 Maxprograms. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/org/documents/epl-v10.html + * + * Contributors: + * Maxprograms - initial API and implementation + *******************************************************************************/ +package com.maxprograms.converters; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.lang.System.Logger; +import java.lang.System.Logger.Level; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Calendar; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.TimeZone; +import java.util.Vector; + +import javax.xml.parsers.ParserConfigurationException; + +import org.xml.sax.SAXException; + +import com.maxprograms.xliff2.FromXliff2; +import com.maxprograms.xml.Attribute; +import com.maxprograms.xml.Catalog; +import com.maxprograms.xml.Document; +import com.maxprograms.xml.Element; +import com.maxprograms.xml.PI; +import com.maxprograms.xml.SAXBuilder; +import com.maxprograms.xml.XMLNode; +import com.maxprograms.xml.XMLUtils; + +public class TmxExporter { + + public static final String DOUBLEPRIME = "\u2033"; + public static final String MATHLT = "\u2039"; + public static final String MATHGT = "\u200B\u203A"; + public static final String GAMP = "\u200B\u203A"; + + static Hashtable docProperties; + private static String sourceLang; + private static String targetLang; + private static String today; + private static int match; + private static String original; + private static int filenumbr; + + private TmxExporter() { + // do not instantiate this class + } + + public static Vector export(String xliff, String tmx, String catalog) { + Vector result = new Vector<>(); + try { + today = getTmxDate(); + filenumbr = 0; + + SAXBuilder builder = new SAXBuilder(); + builder.setEntityResolver(new Catalog(catalog)); + Document doc = builder.build(xliff); + Element root = doc.getRootElement(); + if (root.getAttributeValue("version").equals("2.0")) { + File tmpXliff = File.createTempFile("temp", ".xlf", new File(xliff).getParentFile()); + FromXliff2.run(xliff, tmpXliff.getAbsolutePath(), catalog); + doc = builder.build(tmpXliff); + root = doc.getRootElement(); + Files.delete(Paths.get(tmpXliff.toURI())); + } + + try (FileOutputStream output = new FileOutputStream(tmx)) { + Element firstFile = root.getChild("file"); + + docProperties = new Hashtable<>(); + List slist = root.getPI("subject"); + if (!slist.isEmpty()) { + docProperties.put("subject", slist.get(0).getData()); + } else { + docProperties.put("subject", ""); + } + List plist = root.getPI("project"); + if (!plist.isEmpty()) { + docProperties.put("project", plist.get(0).getData()); + } else { + docProperties.put("project", ""); + } + List clist = root.getPI("customer"); + if (!clist.isEmpty()) { + docProperties.put("customer", clist.get(0).getData()); + } else { + docProperties.put("customer", ""); + } + + sourceLang = firstFile.getAttributeValue("source-language"); + + writeString(output, "\n"); + writeString(output, + "\n"); + writeString(output, "\n"); + writeString(output, + "
\n" + + "
\n"); + writeString(output, "\n"); + + List files = root.getChildren("file"); + Iterator fileiterator = files.iterator(); + while (fileiterator.hasNext()) { + Element file = fileiterator.next(); + sourceLang = file.getAttributeValue("source-language"); + targetLang = file.getAttributeValue("target-language"); + original = "" + file.getAttributeValue("original").hashCode(); + recurse(output, file); + filenumbr++; + } + writeString(output, "\n"); + writeString(output, "
"); + } + result.add(Constants.SUCCESS); + } catch (IOException | SAXException | ParserConfigurationException e) { + Logger logger = System.getLogger(TmxExporter.class.getName()); + logger.log(Level.ERROR, e.getMessage(), e); + result.add(Constants.ERROR); + result.add(e.getMessage()); + } + return result; + } + + private static void recurse(FileOutputStream output, Element e) throws IOException { + List list = e.getChildren(); + Iterator i = list.iterator(); + while (i.hasNext()) { + Element element = i.next(); + if (element.getName().equals("trans-unit")) { + writeSegment(output, element); + } else { + recurse(output, element); + } + } + } + + private static void writeSegment(FileOutputStream output, Element segment) throws IOException { + + String id = original + "-" + filenumbr + "-" + segment.getAttributeValue("id").hashCode(); + + if (segment.getAttributeValue("approved").equals("yes")) { + Element source = segment.getChild("source"); + if (source.getContent().isEmpty()) { + // empty segment, nothing to export + return; + } + Element target = segment.getChild("target"); + if (target == null) { + return; + } + String srcLang = source.getAttributeValue("xml:lang", ""); + if (srcLang.isEmpty()) { + srcLang = sourceLang; + } + String tgtLang = target.getAttributeValue("xml:lang", ""); + if (tgtLang.isEmpty()) { + tgtLang = targetLang; + } + writeString(output, "\n"); + + String customer = docProperties.get("customer"); + String project = docProperties.get("project"); + String subject = docProperties.get("subject"); + + if (!subject.isEmpty()) { + writeString(output, "" + XMLUtils.cleanText(subject) + "\n"); + } + if (!project.isEmpty()) { + writeString(output, "" + XMLUtils.cleanText(project) + "\n"); + } + if (!customer.isEmpty()) { + writeString(output, "" + XMLUtils.cleanText(customer) + "\n"); + } + + List notes = segment.getChildren("note"); + Iterator it = notes.iterator(); + while (it.hasNext()) { + Element note = it.next(); + String lang = note.getAttributeValue("xml:lang", ""); + if (!lang.isEmpty()) { + lang = " xml:lang=\"" + lang + "\""; + } + writeString(output, "" + XMLUtils.cleanText(note.getText()) + "\n"); + } + String srcText = extractText(source); + String tgtText = extractText(target); + if (!segment.getAttributeValue("xml:space", "default").equals("preserve")) { + srcText = srcText.trim(); + tgtText = tgtText.trim(); + } + writeString(output, "\n" + srcText + + "\n\n"); + writeString(output, "\n" + tgtText + + "\n\n"); + writeString(output, "\n"); + } + } + + public static String extractText(Element src) { + + String type = src.getName(); + + if (type.equals("source") || type.equals("target")) { + match = 0; + List l = src.getContent(); + Iterator i = l.iterator(); + StringBuilder text = new StringBuilder(); + while (i.hasNext()) { + XMLNode o = i.next(); + switch (o.getNodeType()) { + case XMLNode.TEXT_NODE: + text.append(o.toString()); + break; + case XMLNode.ELEMENT_NODE: + Element e = (Element) o; + text.append(extractText(e)); + break; + default: + // ignore + } + } + return text.toString(); + } + + if (type.equals("bx") || type.equals("ex") || type.equals("ph")) { + List l = src.getContent(); + Iterator i = l.iterator(); + String ctype = src.getAttributeValue("ctype"); + if (!ctype.isEmpty()) { + ctype = " type=\"" + XMLUtils.cleanText(ctype) + "\""; + } + String assoc = src.getAttributeValue("assoc"); + if (!assoc.isEmpty()) { + assoc = " assoc=\"" + XMLUtils.cleanText(assoc) + "\""; + } + String id = ""; + if (type.equals("ph")) { + id = src.getAttributeValue("id"); + if (!id.isEmpty()) { + id = " x=\"" + XMLUtils.cleanText(id) + "\""; + } + } + StringBuilder text = new StringBuilder(); + text.append("'); + while (i.hasNext()) { + XMLNode o = i.next(); + switch (o.getNodeType()) { + case XMLNode.TEXT_NODE: + text.append(o.toString()); + break; + case XMLNode.ELEMENT_NODE: + Element e = (Element) o; + if (e.getName().equals("sub")) { + text.append(extractText(e)); + } + if (!e.getName().equals("mrk")) { + text.append(extractText(e)); + } + break; + default: + // ignore + } + } + return text + ""; + } + + if (type.equals("g") || type.equals("x")) { + String open = "<" + src.getName(); + List atts = src.getAttributes(); + Iterator h = atts.iterator(); + while (h.hasNext()) { + Attribute a = h.next(); + open = open + " " + a.getName() + "=\"" + a.getValue() + "\""; + } + List l = src.getContent(); + if (!l.isEmpty()) { + open = open + ">"; + int i = match; + match++; + String text = "" + XMLUtils.cleanText(open) + + ""; + Iterator k = l.iterator(); + while (k.hasNext()) { + XMLNode n = k.next(); + if (n.getNodeType() == XMLNode.TEXT_NODE) { + text = text + n.toString(); + } + if (n.getNodeType() == XMLNode.ELEMENT_NODE) { + text = text + extractText((Element) n); + } + } + String close = ""; + return text + "" + XMLUtils.cleanText(close) + ""; + } + return "" + XMLUtils.cleanText(open + "/>") + ""; + } + + if (type.equals("it")) { + List l = src.getContent(); + Iterator i = l.iterator(); + String ctype = src.getAttributeValue("ctype", ""); + if (!ctype.isEmpty()) { + ctype = " type=\"" + XMLUtils.cleanText(ctype) + "\""; + } + String pos = src.getAttributeValue("pos", ""); + if (pos.equals("open")) { + pos = " pos=\"begin\""; + } else if (pos.equals("close")) { + pos = " pos=\"end\""; + } + StringBuilder text = new StringBuilder(); + text.append("'); + while (i.hasNext()) { + XMLNode o = i.next(); + switch (o.getNodeType()) { + case XMLNode.TEXT_NODE: + text.append(o.toString()); + break; + case XMLNode.ELEMENT_NODE: + text.append(extractText((Element) o)); + break; + default: + // ignore + } + } + text.append(""); + return text.toString(); + } + + if (type.equals("bpt") || type.equals("ept")) { + List l = src.getContent(); + Iterator i = l.iterator(); + String ctype = src.getAttributeValue("ctype", ""); + if (!ctype.isEmpty()) { + ctype = " type=\"" + XMLUtils.cleanText(ctype) + "\""; + } + String rid = src.getAttributeValue("rid", ""); + if (!rid.isEmpty()) { + rid = " i=\"" + XMLUtils.cleanText(rid) + "\""; + } else { + rid = " i=\"" + XMLUtils.cleanText(src.getAttributeValue("id", "")) + "\""; + } + StringBuilder text = new StringBuilder(); + text.append('<'); + text.append(type); + text.append(ctype); + text.append(rid); + text.append('>'); + while (i.hasNext()) { + XMLNode o = i.next(); + switch (o.getNodeType()) { + case XMLNode.TEXT_NODE: + text.append( o.toString()); + break; + case XMLNode.ELEMENT_NODE: + text.append(extractText((Element) o)); + break; + default: + // ignore + } + } + text.append("'); + return text.toString(); + } + + if (type.equals("sub")) { + List l = src.getContent(); + Iterator i = l.iterator(); + StringBuilder text = new StringBuilder(); + text.append(""); + while (i.hasNext()) { + XMLNode o = i.next(); + switch (o.getNodeType()) { + case XMLNode.TEXT_NODE: + text.append(o.toString()); + break; + case XMLNode.ELEMENT_NODE: + Element e = (Element) o; + if (!e.getName().equals("mrk")) { + text.append(extractText(e)); + } + break; + default: + // ignore + } + } + text.append(""); + return text.toString(); + } + if (type.equals("mrk")) { + if (src.getAttributeValue("mtype", "").equals("term")) { + // ignore terminology entries + return XMLUtils.cleanText(src.getText()); + } + if (src.getAttributeValue("mtype", "").equals("protected")) { + String ts = src.getAttributeValue("ts", ""); + ts = restoreChars(ts).trim(); + String name = ""; + for (int i = 1; i < ts.length(); i++) { + if (Character.isSpaceChar(ts.charAt(i))) { + break; + } + name = name + ts.charAt(i); + } + return "" + XMLUtils.cleanText(ts) + "" + XMLUtils.cleanText(src.getText()) + + "" + XMLUtils.cleanText("") + ""; + } + return "" + + XMLUtils.cleanText(src.getText()) + ""; + } + return null; + } + + private static String restoreChars(String string) { + String result = string.replaceAll(MATHLT, "<"); + result = result.replaceAll(MATHGT, ">"); + result = result.replaceAll(DOUBLEPRIME, "\""); + return result.replaceAll(GAMP, "&"); + } + + private static void writeString(FileOutputStream output, String input) throws IOException { + output.write(input.getBytes(StandardCharsets.UTF_8)); + } + + public static String getTmxDate() { + Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT")); + String sec = (calendar.get(Calendar.SECOND) < 10 ? "0" : "") + calendar.get(Calendar.SECOND); + String min = (calendar.get(Calendar.MINUTE) < 10 ? "0" : "") + calendar.get(Calendar.MINUTE); + String hour = (calendar.get(Calendar.HOUR_OF_DAY) < 10 ? "0" : "") + calendar.get(Calendar.HOUR_OF_DAY); + String mday = (calendar.get(Calendar.DATE) < 10 ? "0" : "") + calendar.get(Calendar.DATE); + String mon = (calendar.get(Calendar.MONTH) < 9 ? "0" : "") + (calendar.get(Calendar.MONTH) + 1); + String longyear = "" + calendar.get(Calendar.YEAR); + + return longyear + mon + mday + "T" + hour + min + sec + "Z"; + } } \ No newline at end of file diff --git a/src/com/maxprograms/converters/ditamap/DitaMap2Xliff.java b/src/com/maxprograms/converters/ditamap/DitaMap2Xliff.java index 7384cd14..98dd8f31 100644 --- a/src/com/maxprograms/converters/ditamap/DitaMap2Xliff.java +++ b/src/com/maxprograms/converters/ditamap/DitaMap2Xliff.java @@ -1,663 +1,664 @@ -/******************************************************************************* - * Copyright (c) 2003-2019 Maxprograms. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 1.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/org/documents/epl-v10.html - * - * Contributors: - * Maxprograms - initial API and implementation - *******************************************************************************/ -package com.maxprograms.converters.ditamap; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.charset.Charset; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.HashSet; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.List; -import java.util.Set; -import java.util.StringTokenizer; -import java.util.Vector; -import java.lang.System.Logger.Level; -import java.lang.System.Logger; - -import javax.xml.parsers.ParserConfigurationException; - -import org.xml.sax.SAXException; - -import com.maxprograms.converters.EncodingResolver; -import com.maxprograms.converters.FileFormats; -import com.maxprograms.converters.Utils; -import com.maxprograms.converters.xml.Xml2Xliff; -import com.maxprograms.xml.Attribute; -import com.maxprograms.xml.Catalog; -import com.maxprograms.xml.Document; -import com.maxprograms.xml.Element; -import com.maxprograms.xml.Indenter; -import com.maxprograms.xml.PI; -import com.maxprograms.xml.SAXBuilder; -import com.maxprograms.xml.SilentErrorHandler; -import com.maxprograms.xml.XMLNode; -import com.maxprograms.xml.XMLOutputter; - -public class DitaMap2Xliff { - - private static final Logger LOGGER = System.getLogger(DitaMap2Xliff.class.getName()); - - private static Element mergedRoot; - private static SAXBuilder builder; - private static boolean hasConref; - private static Scope rootScope; - private static boolean hasConKeyRef; - private static Hashtable> excludeTable; - private static Hashtable> includeTable; - private static boolean filterAttributes; - private static boolean elementsExcluded; - - private DitaMap2Xliff() { - // do not instantiate this class - // use run method instead - } - - public static Vector run(Hashtable params) { - Vector result = new Vector<>(); - try { - String xliffFile = params.get("xliff"); - String skeleton = params.get("skeleton"); - Catalog catalog = new Catalog(params.get("catalog")); - String mapFile = params.get("source"); - - DitaParser parser = new DitaParser(); - Vector filesMap = parser.run(params); - rootScope = parser.getScope(); - - builder = new SAXBuilder(); - builder.setEntityResolver(catalog); - builder.preserveCustomAttributes(true); - builder.setErrorHandler(new SilentErrorHandler()); - - Vector xliffs = new Vector<>(); - Vector skels = new Vector<>(); - - String ditaval = params.get("ditaval"); - if (ditaval != null) { - parseDitaVal(ditaval, catalog); - } - - for (int i = 0; i < filesMap.size(); i++) { - String file = filesMap.get(i); - String source = ""; - try { - source = checkConref(file); - } catch (SkipException skip) { - // skip untranslatable files - continue; - } catch (Exception ex) { - continue; - } - if (excludeTable != null) { - try { - source = removeFiltered(source); - } catch (Exception sax) { - // skip directly referenced images - continue; - } - } - try { - source = checkConKeyRef(source); - } catch (Exception sax) { - // skip directly referenced images - continue; - } - - File sklParent = new File(skeleton).getParentFile(); - if (!sklParent.exists()) { - sklParent.mkdirs(); - } - File skl = File.createTempFile("dita", ".skl", sklParent); - skels.add(skl.getAbsolutePath()); - File xlf = File.createTempFile("dita", ".xlf", new File(skeleton).getParentFile()); - xlf.deleteOnExit(); - xliffs.add(xlf.getAbsolutePath()); - - Charset encoding = EncodingResolver.getEncoding(source, FileFormats.XML); - Hashtable params2 = new Hashtable<>(); - params2.put("source", source); - params2.put("xliff", xlf.getAbsolutePath()); - params2.put("skeleton", skl.getAbsolutePath()); - params2.put("srcLang", params.get("srcLang")); - String tgtLang = params.get("tgtLang"); - if (tgtLang != null) { - params2.put("tgtLang", tgtLang); - } - params2.put("catalog", params.get("catalog")); - params2.put("srcEncoding", encoding.name()); - params2.put("srxFile", params.get("srxFile")); - params2.put("paragraph", params.get("paragraph")); - params2.put("dita_based", "yes"); - String tComments = params.get("translateComments"); - if (tComments != null) { - params2.put("translateComments", tComments); - } - Vector res = Xml2Xliff.run(params2); - if (!"0".equals(res.get(0))) { - return res; - } - if (!source.equals(filesMap.get(i))) { - // original has conref - fixSource(xlf.getAbsolutePath(), filesMap.get(i)); - } - - } - - Document merged = new Document(null, "xliff", null, null); - mergedRoot = merged.getRootElement(); - mergedRoot.setAttribute("version", "1.2"); - mergedRoot.setAttribute("xmlns", "urn:oasis:names:tc:xliff:document:1.2"); - mergedRoot.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); - mergedRoot.setAttribute("xsi:schemaLocation", - "urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd"); - mergedRoot.addContent("\n"); - mergedRoot.addContent(new PI("encoding", params.get("srcEncoding"))); - - for (int i = 0; i < xliffs.size(); i++) { - mergedRoot.addContent("\n"); - addFile(xliffs.get(i), mapFile); - } - - // output final XLIFF - - XMLOutputter outputter = new XMLOutputter(); - Indenter.indent(mergedRoot, 2); - outputter.preserveSpace(true); - try (FileOutputStream output = new FileOutputStream(xliffFile)) { - outputter.output(merged, output); - } - result.add("0"); - } catch (Exception e) { - LOGGER.log(Level.ERROR, "Error converting DITA Map", e); - result.add("1"); - result.add(e.getMessage()); - } - return result; - } - - private static String removeFiltered(String source) throws IOException, SAXException, ParserConfigurationException { - Document doc = builder.build(source); - Element root = doc.getRootElement(); - elementsExcluded = false; - recurseExcluding(root); - if (elementsExcluded) { - Charset encoding = EncodingResolver.getEncoding(source, FileFormats.XML); - File temp = File.createTempFile("temp", ".dita"); - temp.deleteOnExit(); - XMLOutputter outputter = new XMLOutputter(); - outputter.setEncoding(encoding); - outputter.preserveSpace(true); - try (FileOutputStream out = new FileOutputStream(temp)) { - outputter.output(doc, out); - } - return temp.getAbsolutePath(); - } - doc = null; - root = null; - return source; - } - - private static void recurseExcluding(Element root) { - List children = root.getChildren(); - for (int i = 0; i < children.size(); i++) { - Element child = children.get(i); - if (filterOut(child)) { - child.setAttribute("fluentaIgnore", "yes"); - elementsExcluded = true; - } else { - recurseExcluding(child); - } - } - } - - private static String checkConKeyRef(String source) throws IOException, SAXException, ParserConfigurationException { - Document doc = builder.build(source); - Element root = doc.getRootElement(); - hasConKeyRef = false; - fixConKeyRef(root, source, doc); - if (hasConKeyRef) { - Charset encoding = EncodingResolver.getEncoding(source, FileFormats.XML); - File temp = File.createTempFile("temp", ".dita"); - temp.deleteOnExit(); - XMLOutputter outputter = new XMLOutputter(); - outputter.setEncoding(encoding); - outputter.preserveSpace(true); - try (FileOutputStream out = new FileOutputStream(temp)) { - outputter.output(doc, out); - } - return temp.getAbsolutePath(); - } - return source; - } - - private static void fixConKeyRef(Element e, String source, Document doc) - throws IOException, SAXException, ParserConfigurationException { - String conkeyref = e.getAttributeValue("conkeyref", ""); - String conaction = e.getAttributeValue("conaction", ""); - String keyref = e.getAttributeValue("keyref", ""); - if (!conaction.equals("")) { // it's a conref push - conkeyref = ""; - } - if (!conkeyref.isEmpty()) { - if (conkeyref.indexOf('/') != -1) { - String key = conkeyref.substring(0, conkeyref.indexOf('/')); - String id = conkeyref.substring(conkeyref.indexOf('/') + 1); - Key k = rootScope.getKey(key); - String file = k.getHref(); - if (file == null) { - LOGGER.log(Level.WARNING, "Key not defined for conkeyref: \"" + conkeyref + "\""); - return; - } - Element ref = getConKeyReferenced(file, id); - if (ref != null) { - hasConKeyRef = true; - if (e.getChildren().isEmpty()) { - e.setAttribute("status", "removeContent"); - } - e.setContent(ref.getContent()); - if (ref.getAttributeValue("translate", "yes").equals("no") - && e.getAttributeValue("translate", "yes").equals("yes")) { - e.setAttribute("translate", "no"); - e.setAttribute("removeTranslate", "yes"); - } - List children = e.getChildren(); - Iterator its = children.iterator(); - while (its.hasNext()) { - fixConKeyRef(its.next(), file, doc); - } - } else { - LOGGER.log(Level.WARNING, "Invalid conkeyref: \"" + conkeyref + "\" - Element with id=\"" + id - + "\" not found in \"" + file + "\""); - } - } else { - LOGGER.log(Level.WARNING, "Invalid conkeyref: \"" + conkeyref + "\" - Bad format."); - } - - } else if (!keyref.isEmpty() && e.getContent().isEmpty() && keyref.indexOf('/') == -1) { - - Key k = rootScope.getKey(keyref); - - if (k != null) { - if (k.getTopicmeta() != null) { - // empty element that reuses from - Element matched = DitaParser.getMatched(e.getName(), k.getTopicmeta()); - if (matched != null) { - if (e.getChildren().isEmpty()) { - e.setAttribute("status", "removeContent"); - } - e.setContent(matched.getContent()); - if (matched.getAttributeValue("translate", "yes").equals("no") - && e.getAttributeValue("translate", "yes").equals("yes")) { - e.setAttribute("translate", "no"); - e.setAttribute("removeTranslate", "yes"); - } - hasConKeyRef = true; - } else { - if (DitaParser.ditaClass(e, "topic/image") || DitaParser.isImage(e.getName())) { - Element keyword = DitaParser.getMatched("keyword", k.getTopicmeta()); - if (keyword != null) { - Element alt = new Element("alt"); - alt.setContent(keyword.getContent()); - if (keyword.getAttributeValue("translate", "yes").equals("no") - && e.getAttributeValue("translate", "yes").equals("yes")) { - e.setAttribute("translate", "no"); - e.setAttribute("removeTranslate", "yes"); - } - e.addContent(alt); - e.setAttribute("status", "removeContent"); - hasConKeyRef = true; - } - } else if (DitaParser.ditaClass(e, "topic/xref") || DitaParser.ditaClass(e, "topic/link") - || DitaParser.isXref(e.getName()) || DitaParser.isLink(e.getName())) { - Element keyword = DitaParser.getMatched("keyword", k.getTopicmeta()); - if (keyword != null) { - Element alt = new Element("linktext"); - alt.setContent(keyword.getContent()); - if (keyword.getAttributeValue("translate", "yes").equals("no") - && e.getAttributeValue("translate", "yes").equals("yes")) { - e.setAttribute("translate", "no"); - e.setAttribute("removeTranslate", "yes"); - } - e.addContent(alt); - e.setAttribute("status", "removeContent"); - hasConKeyRef = true; - } - } else { - Element keyword = DitaParser.getMatched("keyword", k.getTopicmeta()); - if (keyword != null) { - e.addContent(keyword); - if (keyword.getAttributeValue("translate", "yes").equals("no") - && e.getAttributeValue("translate", "yes").equals("yes")) { - e.setAttribute("translate", "no"); - e.setAttribute("removeTranslate", "yes"); - } - e.setAttribute("status", "removeContent"); - hasConKeyRef = true; - } - } - } - } else { - String href = k.getHref(); - try { - Document d = builder.build(href); - Element r = d.getRootElement(); - Element referenced = r; - if (keyref.indexOf('/') != -1) { - String id = keyref.substring(keyref.indexOf('/') + 1); - referenced = locateReferenced(r, id); - } - if (referenced != null && e.getName().equals(referenced.getName())) { - if (e.getChildren().isEmpty()) { - e.setAttribute("status", "removeContent"); - } - e.setContent(referenced.getContent()); - hasConKeyRef = true; - } else { - if (e.getName().equals("abbreviated-form") && referenced != null - && referenced.getName().equals("glossentry")) { - List content = getGlossContent(referenced); - if (content != null && !content.isEmpty()) { - if (e.getChildren().isEmpty()) { - e.setAttribute("status", "removeContent"); - } - e.setContent(content); - hasConKeyRef = true; - } - } - } - List children = e.getChildren(); - Iterator it = children.iterator(); - while (it.hasNext()) { - fixConKeyRef(it.next(), source, doc); - } - } catch (Exception ex) { - // do nothing - } - } - } - } else { - List children = e.getChildren(); - Iterator it = children.iterator(); - while (it.hasNext()) { - fixConKeyRef(it.next(), source, doc); - } - } - } - - private static List getGlossContent(Element glossentry) { - Element e = getGlossComponent("glossSurfaceForm", glossentry); - if (e != null) { - return e.getContent(); - } - e = getGlossComponent("glossAbbreviation", glossentry); - if (e != null) { - return e.getContent(); - } - e = getGlossComponent("glossAlt", glossentry); - if (e != null) { - return e.getContent(); - } - e = getGlossComponent("glossterm", glossentry); - if (e != null) { - return e.getContent(); - } - return null; - } - - private static Element getGlossComponent(String component, Element e) { - if (e.getName().equals(component)) { - return e; - } - List children = e.getChildren(); - for (int i = 0; i < children.size(); i++) { - Element g = getGlossComponent(component, children.get(i)); - if (g != null) { - return g; - } - } - return null; - } - - private static Element getConKeyReferenced(String file, String id) - throws SAXException, IOException, ParserConfigurationException { - Document doc = builder.build(file); - Element root = doc.getRootElement(); - return locateReferenced(root, id); - } - - private static Element locateReferenced(Element root, String id) { - String current = root.getAttributeValue("id", ""); - if (current.equals(id)) { - return root; - } - List children = root.getChildren(); - Iterator it = children.iterator(); - while (it.hasNext()) { - Element child = it.next(); - Element result = locateReferenced(child, id); - if (result != null) { - return result; - } - } - return null; - } - - private static void fixSource(String xliff, String string) - throws SAXException, IOException, ParserConfigurationException { - Document doc = builder.build(xliff); - Element root = doc.getRootElement(); - root.getChild("file").setAttribute("original", string); - XMLOutputter outputter = new XMLOutputter(); - outputter.preserveSpace(true); - try (FileOutputStream out = new FileOutputStream(xliff)) { - outputter.output(doc, out); - } - } - - private static String checkConref(String source) - throws SkipException, IOException, SAXException, ParserConfigurationException { - Document doc = builder.build(source); - Element root = doc.getRootElement(); - if (root.getAttributeValue("translate", "yes").equalsIgnoreCase("no")) { - throw new SkipException("Untranslatable!"); - } - hasConref = false; - fixConref(root, source, doc); - if (hasConref) { - Charset encoding = EncodingResolver.getEncoding(source, FileFormats.XML); - File temp = File.createTempFile("temp", ".dita"); - temp.deleteOnExit(); - XMLOutputter outputter = new XMLOutputter(); - outputter.setEncoding(encoding); - outputter.preserveSpace(true); - try (FileOutputStream out = new FileOutputStream(temp)) { - outputter.output(doc, out); - } - return temp.getAbsolutePath(); - } - return source; - } - - private static void fixConref(Element e, String source, Document doc) - throws IOException, SAXException, ParserConfigurationException { - String conref = e.getAttributeValue("conref", ""); - String conaction = e.getAttributeValue("conaction", ""); - if (!conaction.equals("")) { // it's a conref push - conref = ""; - } - if (!conref.equals("")) { - if (conref.indexOf('#') != -1) { - String file = conref.substring(0, conref.indexOf('#')); - if (file.length() == 0) { - file = source; - } else { - File f = new File(file); - if (!f.isAbsolute()) { - file = Utils.getAbsolutePath(source, file); - } - } - String id = conref.substring(conref.indexOf('#') + 1); - Element ref = getReferenced(file, id); - if (ref != null) { - hasConref = true; - if (e.getChildren().isEmpty()) { - e.setAttribute("status", "removeContent"); - } - e.setContent(ref.getContent()); - if (ref.getAttributeValue("translate", "yes").equals("no") - && e.getAttributeValue("translate", "yes").equals("yes")) { - e.setAttribute("translate", "no"); - e.setAttribute("removeTranslate", "yes"); - } - List children = e.getChildren(); - Iterator its = children.iterator(); - while (its.hasNext()) { - fixConref(its.next(), file, doc); - } - } - } - } else { - List children = e.getChildren(); - Iterator it = children.iterator(); - while (it.hasNext()) { - fixConref(it.next(), source, doc); - } - } - } - - private static Element getReferenced(String file, String id) - throws SAXException, IOException, ParserConfigurationException { - Document doc = builder.build(file); - Element root = doc.getRootElement(); - String topicId = root.getAttributeValue("id", ""); - if (topicId.equals(id)) { - return root; - } - return locate(root, topicId, id); - } - - private static Element locate(Element root, String topicId, String id) { - String current = root.getAttributeValue("id", ""); - if (id.equals(topicId + "/" + current)) { - return root; - } - List children = root.getChildren(); - Iterator it = children.iterator(); - while (it.hasNext()) { - Element child = it.next(); - Element result = locate(child, topicId, id); - if (result != null) { - return result; - } - } - return null; - } - - private static void addFile(String xliff, String mapFile) - throws SAXException, IOException, ParserConfigurationException { - Document doc = builder.build(xliff); - Element root = doc.getRootElement(); - Element file = root.getChild("file"); - Element newFile = new Element("file"); - newFile.clone(file); - newFile.setAttribute("datatype", "x-ditamap"); - String old = file.getAttributeValue("original"); - newFile.setAttribute("original", Utils.makeRelativePath(mapFile, old)); - List encoding = root.getPI("encoding"); - if (!encoding.isEmpty()) { - newFile.addContent(encoding.get(0)); - } - mergedRoot.addContent(newFile); - File f = new File(xliff); - Files.delete(Paths.get(f.toURI())); - } - - private static void parseDitaVal(String ditaval, Catalog catalog) - throws SAXException, IOException, ParserConfigurationException { - SAXBuilder bder = new SAXBuilder(); - bder.setEntityResolver(catalog); - Document doc = bder.build(ditaval); - Element root = doc.getRootElement(); - if (root.getName().equals("val")) { - List props = root.getChildren("prop"); - Iterator it = props.iterator(); - excludeTable = new Hashtable<>(); - includeTable = new Hashtable<>(); - while (it.hasNext()) { - Element prop = it.next(); - if (prop.getAttributeValue("action", "include").equals("exclude")) { - String att = prop.getAttributeValue("att", ""); - String val = prop.getAttributeValue("val", ""); - if (!att.equals("")) { - Set set = excludeTable.get(att); - if (set == null) { - set = new HashSet<>(); - } - if (!val.equals("")) { - set.add(val); - } - excludeTable.put(att, set); - } - } - if (prop.getAttributeValue("action", "include").equals("include")) { - String att = prop.getAttributeValue("att", ""); - String val = prop.getAttributeValue("val", ""); - if (!att.equals("") && !val.equals("")) { - Set set = includeTable.get(att); - if (set == null) { - set = new HashSet<>(); - } - set.add(val); - includeTable.put(att, set); - } - } - } - filterAttributes = true; - } - } - - private static boolean filterOut(Element e) { - if (filterAttributes) { - List atts = e.getAttributes(); - Iterator it = atts.iterator(); - while (it.hasNext()) { - Attribute a = it.next(); - if (excludeTable.containsKey(a.getName())) { - Set forbidden = excludeTable.get(a.getName()); - if (forbidden.isEmpty() && includeTable.containsKey(a.getName())) { - Set allowed = includeTable.get(a.getName()); - StringTokenizer tokenizer = new StringTokenizer(a.getValue()); - while (tokenizer.hasMoreTokens()) { - String token = tokenizer.nextToken(); - if (allowed.contains(token)) { - return false; - } - } - } - StringTokenizer tokenizer = new StringTokenizer(a.getValue()); - Vector tokens = new Vector<>(); - while (tokenizer.hasMoreTokens()) { - tokens.add(tokenizer.nextToken()); - } - if (forbidden.containsAll(tokens)) { - return true; - } - } - } - } - return false; - } -} +/******************************************************************************* + * Copyright (c) 2003-2019 Maxprograms. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/org/documents/epl-v10.html + * + * Contributors: + * Maxprograms - initial API and implementation + *******************************************************************************/ +package com.maxprograms.converters.ditamap; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.Vector; +import java.lang.System.Logger.Level; +import java.lang.System.Logger; + +import javax.xml.parsers.ParserConfigurationException; + +import org.xml.sax.SAXException; + +import com.maxprograms.converters.Constants; +import com.maxprograms.converters.EncodingResolver; +import com.maxprograms.converters.FileFormats; +import com.maxprograms.converters.Utils; +import com.maxprograms.converters.xml.Xml2Xliff; +import com.maxprograms.xml.Attribute; +import com.maxprograms.xml.Catalog; +import com.maxprograms.xml.Document; +import com.maxprograms.xml.Element; +import com.maxprograms.xml.Indenter; +import com.maxprograms.xml.PI; +import com.maxprograms.xml.SAXBuilder; +import com.maxprograms.xml.SilentErrorHandler; +import com.maxprograms.xml.XMLNode; +import com.maxprograms.xml.XMLOutputter; + +public class DitaMap2Xliff { + + private static final Logger LOGGER = System.getLogger(DitaMap2Xliff.class.getName()); + + private static Element mergedRoot; + private static SAXBuilder builder; + private static boolean hasConref; + private static Scope rootScope; + private static boolean hasConKeyRef; + private static Hashtable> excludeTable; + private static Hashtable> includeTable; + private static boolean filterAttributes; + private static boolean elementsExcluded; + + private DitaMap2Xliff() { + // do not instantiate this class + // use run method instead + } + + public static Vector run(Hashtable params) { + Vector result = new Vector<>(); + try { + String xliffFile = params.get("xliff"); + String skeleton = params.get("skeleton"); + Catalog catalog = new Catalog(params.get("catalog")); + String mapFile = params.get("source"); + + DitaParser parser = new DitaParser(); + Vector filesMap = parser.run(params); + rootScope = parser.getScope(); + + builder = new SAXBuilder(); + builder.setEntityResolver(catalog); + builder.preserveCustomAttributes(true); + builder.setErrorHandler(new SilentErrorHandler()); + + Vector xliffs = new Vector<>(); + Vector skels = new Vector<>(); + + String ditaval = params.get("ditaval"); + if (ditaval != null) { + parseDitaVal(ditaval, catalog); + } + + for (int i = 0; i < filesMap.size(); i++) { + String file = filesMap.get(i); + String source = ""; + try { + source = checkConref(file); + } catch (SkipException skip) { + // skip untranslatable files + continue; + } catch (Exception ex) { + continue; + } + if (excludeTable != null) { + try { + source = removeFiltered(source); + } catch (Exception sax) { + // skip directly referenced images + continue; + } + } + try { + source = checkConKeyRef(source); + } catch (Exception sax) { + // skip directly referenced images + continue; + } + + File sklParent = new File(skeleton).getParentFile(); + if (!sklParent.exists()) { + sklParent.mkdirs(); + } + File skl = File.createTempFile("dita", ".skl", sklParent); + skels.add(skl.getAbsolutePath()); + File xlf = File.createTempFile("dita", ".xlf", new File(skeleton).getParentFile()); + xlf.deleteOnExit(); + xliffs.add(xlf.getAbsolutePath()); + + Charset encoding = EncodingResolver.getEncoding(source, FileFormats.XML); + Hashtable params2 = new Hashtable<>(); + params2.put("source", source); + params2.put("xliff", xlf.getAbsolutePath()); + params2.put("skeleton", skl.getAbsolutePath()); + params2.put("srcLang", params.get("srcLang")); + String tgtLang = params.get("tgtLang"); + if (tgtLang != null) { + params2.put("tgtLang", tgtLang); + } + params2.put("catalog", params.get("catalog")); + params2.put("srcEncoding", encoding.name()); + params2.put("srxFile", params.get("srxFile")); + params2.put("paragraph", params.get("paragraph")); + params2.put("dita_based", "yes"); + String tComments = params.get("translateComments"); + if (tComments != null) { + params2.put("translateComments", tComments); + } + Vector res = Xml2Xliff.run(params2); + if (!Constants.SUCCESS.equals(res.get(0))) { + return res; + } + if (!source.equals(filesMap.get(i))) { + // original has conref + fixSource(xlf.getAbsolutePath(), filesMap.get(i)); + } + + } + + Document merged = new Document(null, "xliff", null, null); + mergedRoot = merged.getRootElement(); + mergedRoot.setAttribute("version", "1.2"); + mergedRoot.setAttribute("xmlns", "urn:oasis:names:tc:xliff:document:1.2"); + mergedRoot.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); + mergedRoot.setAttribute("xsi:schemaLocation", + "urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd"); + mergedRoot.addContent("\n"); + mergedRoot.addContent(new PI("encoding", params.get("srcEncoding"))); + + for (int i = 0; i < xliffs.size(); i++) { + mergedRoot.addContent("\n"); + addFile(xliffs.get(i), mapFile); + } + + // output final XLIFF + + XMLOutputter outputter = new XMLOutputter(); + Indenter.indent(mergedRoot, 2); + outputter.preserveSpace(true); + try (FileOutputStream output = new FileOutputStream(xliffFile)) { + outputter.output(merged, output); + } + result.add(Constants.SUCCESS); + } catch (Exception e) { + LOGGER.log(Level.ERROR, "Error converting DITA Map", e); + result.add(Constants.ERROR); + result.add(e.getMessage()); + } + return result; + } + + private static String removeFiltered(String source) throws IOException, SAXException, ParserConfigurationException { + Document doc = builder.build(source); + Element root = doc.getRootElement(); + elementsExcluded = false; + recurseExcluding(root); + if (elementsExcluded) { + Charset encoding = EncodingResolver.getEncoding(source, FileFormats.XML); + File temp = File.createTempFile("temp", ".dita"); + temp.deleteOnExit(); + XMLOutputter outputter = new XMLOutputter(); + outputter.setEncoding(encoding); + outputter.preserveSpace(true); + try (FileOutputStream out = new FileOutputStream(temp)) { + outputter.output(doc, out); + } + return temp.getAbsolutePath(); + } + doc = null; + root = null; + return source; + } + + private static void recurseExcluding(Element root) { + List children = root.getChildren(); + for (int i = 0; i < children.size(); i++) { + Element child = children.get(i); + if (filterOut(child)) { + child.setAttribute("fluentaIgnore", "yes"); + elementsExcluded = true; + } else { + recurseExcluding(child); + } + } + } + + private static String checkConKeyRef(String source) throws IOException, SAXException, ParserConfigurationException { + Document doc = builder.build(source); + Element root = doc.getRootElement(); + hasConKeyRef = false; + fixConKeyRef(root, source, doc); + if (hasConKeyRef) { + Charset encoding = EncodingResolver.getEncoding(source, FileFormats.XML); + File temp = File.createTempFile("temp", ".dita"); + temp.deleteOnExit(); + XMLOutputter outputter = new XMLOutputter(); + outputter.setEncoding(encoding); + outputter.preserveSpace(true); + try (FileOutputStream out = new FileOutputStream(temp)) { + outputter.output(doc, out); + } + return temp.getAbsolutePath(); + } + return source; + } + + private static void fixConKeyRef(Element e, String source, Document doc) + throws IOException, SAXException, ParserConfigurationException { + String conkeyref = e.getAttributeValue("conkeyref", ""); + String conaction = e.getAttributeValue("conaction", ""); + String keyref = e.getAttributeValue("keyref", ""); + if (!conaction.equals("")) { // it's a conref push + conkeyref = ""; + } + if (!conkeyref.isEmpty()) { + if (conkeyref.indexOf('/') != -1) { + String key = conkeyref.substring(0, conkeyref.indexOf('/')); + String id = conkeyref.substring(conkeyref.indexOf('/') + 1); + Key k = rootScope.getKey(key); + String file = k.getHref(); + if (file == null) { + LOGGER.log(Level.WARNING, "Key not defined for conkeyref: \"" + conkeyref + "\""); + return; + } + Element ref = getConKeyReferenced(file, id); + if (ref != null) { + hasConKeyRef = true; + if (e.getChildren().isEmpty()) { + e.setAttribute("status", "removeContent"); + } + e.setContent(ref.getContent()); + if (ref.getAttributeValue("translate", "yes").equals("no") + && e.getAttributeValue("translate", "yes").equals("yes")) { + e.setAttribute("translate", "no"); + e.setAttribute("removeTranslate", "yes"); + } + List children = e.getChildren(); + Iterator its = children.iterator(); + while (its.hasNext()) { + fixConKeyRef(its.next(), file, doc); + } + } else { + LOGGER.log(Level.WARNING, "Invalid conkeyref: \"" + conkeyref + "\" - Element with id=\"" + id + + "\" not found in \"" + file + "\""); + } + } else { + LOGGER.log(Level.WARNING, "Invalid conkeyref: \"" + conkeyref + "\" - Bad format."); + } + + } else if (!keyref.isEmpty() && e.getContent().isEmpty() && keyref.indexOf('/') == -1) { + + Key k = rootScope.getKey(keyref); + + if (k != null) { + if (k.getTopicmeta() != null) { + // empty element that reuses from + Element matched = DitaParser.getMatched(e.getName(), k.getTopicmeta()); + if (matched != null) { + if (e.getChildren().isEmpty()) { + e.setAttribute("status", "removeContent"); + } + e.setContent(matched.getContent()); + if (matched.getAttributeValue("translate", "yes").equals("no") + && e.getAttributeValue("translate", "yes").equals("yes")) { + e.setAttribute("translate", "no"); + e.setAttribute("removeTranslate", "yes"); + } + hasConKeyRef = true; + } else { + if (DitaParser.ditaClass(e, "topic/image") || DitaParser.isImage(e.getName())) { + Element keyword = DitaParser.getMatched("keyword", k.getTopicmeta()); + if (keyword != null) { + Element alt = new Element("alt"); + alt.setContent(keyword.getContent()); + if (keyword.getAttributeValue("translate", "yes").equals("no") + && e.getAttributeValue("translate", "yes").equals("yes")) { + e.setAttribute("translate", "no"); + e.setAttribute("removeTranslate", "yes"); + } + e.addContent(alt); + e.setAttribute("status", "removeContent"); + hasConKeyRef = true; + } + } else if (DitaParser.ditaClass(e, "topic/xref") || DitaParser.ditaClass(e, "topic/link") + || DitaParser.isXref(e.getName()) || DitaParser.isLink(e.getName())) { + Element keyword = DitaParser.getMatched("keyword", k.getTopicmeta()); + if (keyword != null) { + Element alt = new Element("linktext"); + alt.setContent(keyword.getContent()); + if (keyword.getAttributeValue("translate", "yes").equals("no") + && e.getAttributeValue("translate", "yes").equals("yes")) { + e.setAttribute("translate", "no"); + e.setAttribute("removeTranslate", "yes"); + } + e.addContent(alt); + e.setAttribute("status", "removeContent"); + hasConKeyRef = true; + } + } else { + Element keyword = DitaParser.getMatched("keyword", k.getTopicmeta()); + if (keyword != null) { + e.addContent(keyword); + if (keyword.getAttributeValue("translate", "yes").equals("no") + && e.getAttributeValue("translate", "yes").equals("yes")) { + e.setAttribute("translate", "no"); + e.setAttribute("removeTranslate", "yes"); + } + e.setAttribute("status", "removeContent"); + hasConKeyRef = true; + } + } + } + } else { + String href = k.getHref(); + try { + Document d = builder.build(href); + Element r = d.getRootElement(); + Element referenced = r; + if (keyref.indexOf('/') != -1) { + String id = keyref.substring(keyref.indexOf('/') + 1); + referenced = locateReferenced(r, id); + } + if (referenced != null && e.getName().equals(referenced.getName())) { + if (e.getChildren().isEmpty()) { + e.setAttribute("status", "removeContent"); + } + e.setContent(referenced.getContent()); + hasConKeyRef = true; + } else { + if (e.getName().equals("abbreviated-form") && referenced != null + && referenced.getName().equals("glossentry")) { + List content = getGlossContent(referenced); + if (content != null && !content.isEmpty()) { + if (e.getChildren().isEmpty()) { + e.setAttribute("status", "removeContent"); + } + e.setContent(content); + hasConKeyRef = true; + } + } + } + List children = e.getChildren(); + Iterator it = children.iterator(); + while (it.hasNext()) { + fixConKeyRef(it.next(), source, doc); + } + } catch (Exception ex) { + // do nothing + } + } + } + } else { + List children = e.getChildren(); + Iterator it = children.iterator(); + while (it.hasNext()) { + fixConKeyRef(it.next(), source, doc); + } + } + } + + private static List getGlossContent(Element glossentry) { + Element e = getGlossComponent("glossSurfaceForm", glossentry); + if (e != null) { + return e.getContent(); + } + e = getGlossComponent("glossAbbreviation", glossentry); + if (e != null) { + return e.getContent(); + } + e = getGlossComponent("glossAlt", glossentry); + if (e != null) { + return e.getContent(); + } + e = getGlossComponent("glossterm", glossentry); + if (e != null) { + return e.getContent(); + } + return null; + } + + private static Element getGlossComponent(String component, Element e) { + if (e.getName().equals(component)) { + return e; + } + List children = e.getChildren(); + for (int i = 0; i < children.size(); i++) { + Element g = getGlossComponent(component, children.get(i)); + if (g != null) { + return g; + } + } + return null; + } + + private static Element getConKeyReferenced(String file, String id) + throws SAXException, IOException, ParserConfigurationException { + Document doc = builder.build(file); + Element root = doc.getRootElement(); + return locateReferenced(root, id); + } + + private static Element locateReferenced(Element root, String id) { + String current = root.getAttributeValue("id", ""); + if (current.equals(id)) { + return root; + } + List children = root.getChildren(); + Iterator it = children.iterator(); + while (it.hasNext()) { + Element child = it.next(); + Element result = locateReferenced(child, id); + if (result != null) { + return result; + } + } + return null; + } + + private static void fixSource(String xliff, String string) + throws SAXException, IOException, ParserConfigurationException { + Document doc = builder.build(xliff); + Element root = doc.getRootElement(); + root.getChild("file").setAttribute("original", string); + XMLOutputter outputter = new XMLOutputter(); + outputter.preserveSpace(true); + try (FileOutputStream out = new FileOutputStream(xliff)) { + outputter.output(doc, out); + } + } + + private static String checkConref(String source) + throws SkipException, IOException, SAXException, ParserConfigurationException { + Document doc = builder.build(source); + Element root = doc.getRootElement(); + if (root.getAttributeValue("translate", "yes").equalsIgnoreCase("no")) { + throw new SkipException("Untranslatable!"); + } + hasConref = false; + fixConref(root, source, doc); + if (hasConref) { + Charset encoding = EncodingResolver.getEncoding(source, FileFormats.XML); + File temp = File.createTempFile("temp", ".dita"); + temp.deleteOnExit(); + XMLOutputter outputter = new XMLOutputter(); + outputter.setEncoding(encoding); + outputter.preserveSpace(true); + try (FileOutputStream out = new FileOutputStream(temp)) { + outputter.output(doc, out); + } + return temp.getAbsolutePath(); + } + return source; + } + + private static void fixConref(Element e, String source, Document doc) + throws IOException, SAXException, ParserConfigurationException { + String conref = e.getAttributeValue("conref", ""); + String conaction = e.getAttributeValue("conaction", ""); + if (!conaction.equals("")) { // it's a conref push + conref = ""; + } + if (!conref.equals("")) { + if (conref.indexOf('#') != -1) { + String file = conref.substring(0, conref.indexOf('#')); + if (file.length() == 0) { + file = source; + } else { + File f = new File(file); + if (!f.isAbsolute()) { + file = Utils.getAbsolutePath(source, file); + } + } + String id = conref.substring(conref.indexOf('#') + 1); + Element ref = getReferenced(file, id); + if (ref != null) { + hasConref = true; + if (e.getChildren().isEmpty()) { + e.setAttribute("status", "removeContent"); + } + e.setContent(ref.getContent()); + if (ref.getAttributeValue("translate", "yes").equals("no") + && e.getAttributeValue("translate", "yes").equals("yes")) { + e.setAttribute("translate", "no"); + e.setAttribute("removeTranslate", "yes"); + } + List children = e.getChildren(); + Iterator its = children.iterator(); + while (its.hasNext()) { + fixConref(its.next(), file, doc); + } + } + } + } else { + List children = e.getChildren(); + Iterator it = children.iterator(); + while (it.hasNext()) { + fixConref(it.next(), source, doc); + } + } + } + + private static Element getReferenced(String file, String id) + throws SAXException, IOException, ParserConfigurationException { + Document doc = builder.build(file); + Element root = doc.getRootElement(); + String topicId = root.getAttributeValue("id", ""); + if (topicId.equals(id)) { + return root; + } + return locate(root, topicId, id); + } + + private static Element locate(Element root, String topicId, String id) { + String current = root.getAttributeValue("id", ""); + if (id.equals(topicId + "/" + current)) { + return root; + } + List children = root.getChildren(); + Iterator it = children.iterator(); + while (it.hasNext()) { + Element child = it.next(); + Element result = locate(child, topicId, id); + if (result != null) { + return result; + } + } + return null; + } + + private static void addFile(String xliff, String mapFile) + throws SAXException, IOException, ParserConfigurationException { + Document doc = builder.build(xliff); + Element root = doc.getRootElement(); + Element file = root.getChild("file"); + Element newFile = new Element("file"); + newFile.clone(file); + newFile.setAttribute("datatype", "x-ditamap"); + String old = file.getAttributeValue("original"); + newFile.setAttribute("original", Utils.makeRelativePath(mapFile, old)); + List encoding = root.getPI("encoding"); + if (!encoding.isEmpty()) { + newFile.addContent(encoding.get(0)); + } + mergedRoot.addContent(newFile); + File f = new File(xliff); + Files.delete(Paths.get(f.toURI())); + } + + private static void parseDitaVal(String ditaval, Catalog catalog) + throws SAXException, IOException, ParserConfigurationException { + SAXBuilder bder = new SAXBuilder(); + bder.setEntityResolver(catalog); + Document doc = bder.build(ditaval); + Element root = doc.getRootElement(); + if (root.getName().equals("val")) { + List props = root.getChildren("prop"); + Iterator it = props.iterator(); + excludeTable = new Hashtable<>(); + includeTable = new Hashtable<>(); + while (it.hasNext()) { + Element prop = it.next(); + if (prop.getAttributeValue("action", "include").equals("exclude")) { + String att = prop.getAttributeValue("att", ""); + String val = prop.getAttributeValue("val", ""); + if (!att.equals("")) { + Set set = excludeTable.get(att); + if (set == null) { + set = new HashSet<>(); + } + if (!val.equals("")) { + set.add(val); + } + excludeTable.put(att, set); + } + } + if (prop.getAttributeValue("action", "include").equals("include")) { + String att = prop.getAttributeValue("att", ""); + String val = prop.getAttributeValue("val", ""); + if (!att.equals("") && !val.equals("")) { + Set set = includeTable.get(att); + if (set == null) { + set = new HashSet<>(); + } + set.add(val); + includeTable.put(att, set); + } + } + } + filterAttributes = true; + } + } + + private static boolean filterOut(Element e) { + if (filterAttributes) { + List atts = e.getAttributes(); + Iterator it = atts.iterator(); + while (it.hasNext()) { + Attribute a = it.next(); + if (excludeTable.containsKey(a.getName())) { + Set forbidden = excludeTable.get(a.getName()); + if (forbidden.isEmpty() && includeTable.containsKey(a.getName())) { + Set allowed = includeTable.get(a.getName()); + StringTokenizer tokenizer = new StringTokenizer(a.getValue()); + while (tokenizer.hasMoreTokens()) { + String token = tokenizer.nextToken(); + if (allowed.contains(token)) { + return false; + } + } + } + StringTokenizer tokenizer = new StringTokenizer(a.getValue()); + Vector tokens = new Vector<>(); + while (tokenizer.hasMoreTokens()) { + tokens.add(tokenizer.nextToken()); + } + if (forbidden.containsAll(tokens)) { + return true; + } + } + } + } + return false; + } +} diff --git a/src/com/maxprograms/converters/ditamap/Xliff2DitaMap.java b/src/com/maxprograms/converters/ditamap/Xliff2DitaMap.java index 317cee85..3c5ccb50 100644 --- a/src/com/maxprograms/converters/ditamap/Xliff2DitaMap.java +++ b/src/com/maxprograms/converters/ditamap/Xliff2DitaMap.java @@ -1,251 +1,252 @@ -/******************************************************************************* - * Copyright (c) 2003-2019 Maxprograms. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 1.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/org/documents/epl-v10.html - * - * Contributors: - * Maxprograms - initial API and implementation - *******************************************************************************/ -package com.maxprograms.converters.ditamap; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.Enumeration; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.List; -import java.util.Vector; -import java.lang.System.Logger.Level; -import java.lang.System.Logger; - -import javax.xml.parsers.ParserConfigurationException; - -import org.xml.sax.SAXException; - -import com.maxprograms.converters.StringConverter; -import com.maxprograms.converters.Utils; -import com.maxprograms.converters.xml.Xliff2Xml; -import com.maxprograms.xml.CData; -import com.maxprograms.xml.Catalog; -import com.maxprograms.xml.Document; -import com.maxprograms.xml.Element; -import com.maxprograms.xml.Indenter; -import com.maxprograms.xml.PI; -import com.maxprograms.xml.SAXBuilder; -import com.maxprograms.xml.TextNode; -import com.maxprograms.xml.XMLNode; -import com.maxprograms.xml.XMLOutputter; - -public class Xliff2DitaMap { - - private static Hashtable filesTable; - - public static Vector run(Hashtable params) { - Vector result = new Vector<>(); - String xliffFile = ""; - Xliff2DitaMap instance = new Xliff2DitaMap(); - try { - xliffFile = params.get("xliff"); - String outputFile = params.get("backfile"); - filesTable = new Hashtable<>(); - String catalog = params.get("catalog"); - SAXBuilder builder = new SAXBuilder(); - builder.preserveCustomAttributes(true); - builder.setEntityResolver(new Catalog(catalog)); - Document doc = builder.build(xliffFile); - Element root = doc.getRootElement(); - List files = root.getChildren("file"); - Iterator it = files.iterator(); - while (it.hasNext()) { - saveFile(it.next(), xliffFile); - } - String tgtlang = files.get(0).getAttributeValue("target-language", - files.get(0).getAttributeValue("source-language")); - Enumeration keys = filesTable.keys(); - XMLOutputter outputter = new XMLOutputter(); - outputter.preserveSpace(true); - while (keys.hasMoreElements()) { - String topicFile = keys.nextElement(); - String[] values = filesTable.get(topicFile); - Hashtable params2 = new Hashtable<>(); - params2.put("xliff", values[0]); - params2.put("skeleton", values[1]); - File folder = new File(outputFile); - File p = folder.getParentFile(); - if (p == null) { - p = new File(System.getProperty("user.dir")); - } - if (!p.exists()) { - p.mkdirs(); - } - params2.put("backfile", outputFile); - params2.put("encoding", params.get("encoding")); - params2.put("catalog", params.get("catalog")); - params2.put("dita_based", "yes"); - Vector res = Xliff2Xml.run(params2); - if (!"0".equals(res.get(0))) { - return res; - } - - doc = builder.build(outputFile); - Element r = doc.getRootElement(); - - List ish = doc.getPI("ish"); - String id = r.getAttributeValue("id", ""); - - if (!ish.isEmpty() || id.startsWith("GUID-")) { - restoreGUID(r); - } - - r.setAttribute("xml:lang", tgtlang); - Indenter.indent(r, 2); - instance.cleanConref(r); - try (FileOutputStream out = new FileOutputStream(outputFile)) { - outputter.output(doc, out); - } - File f = new File(values[0]); - Files.delete(Paths.get(f.toURI())); - } - - result.add("0"); - } catch (IOException | SAXException | ParserConfigurationException e) { - Logger logger = System.getLogger(Xliff2DitaMap.class.getName()); - logger.log(Level.ERROR, "Error merging DITA Map", e); - result.add("1"); - result.add(e.getMessage()); - } - return result; - } - - private static void restoreGUID(Element r) { - String href = r.getAttributeValue("href", ""); - if (!href.isEmpty()) { - int index = href.indexOf("GUID"); - if (index != -1) { - int end = href.indexOf('=', index + 1); - if (end != -1) { - String guid = href.substring(index, end); - r.setAttribute("href", guid); - } - } - } - String conref = r.getAttributeValue("conref", ""); - int index = conref.indexOf('='); - if (!conref.isEmpty() && conref.startsWith("GUID-") && index != -1) { - String guid = conref.substring(0, index); - index = conref.indexOf('#'); - if (index != -1) { - String rest = conref.substring(index); - r.setAttribute("conref", guid + rest); - } else { - r.setAttribute("conref", guid); - } - } - r.removeAttribute("class"); - r.removeAttribute("ditaarch:DITAArchVersion"); - r.removeAttribute("domains"); - List children = r.getChildren(); - for (int i = 0; i < children.size(); i++) { - restoreGUID(children.get(i)); - } - } - - private void cleanConref(Element e) { - if (!e.getAttributeValue("fluentaIgnore").isEmpty()) { - e.removeAttribute("fluentaIgnore"); - } - if (!e.getAttributeValue("keyref", "").equals("") - && e.getAttributeValue("status", "").equals("removeContent")) { //$NON-NLS-3$ - e.setContent(new Vector<>()); - e.removeAttribute("status"); - } - if (!e.getAttributeValue("conref", "").equals("") && e.getAttributeValue("conaction", "").equals("")) { //$NON-NLS-6$ - emptyElement(e); - } - if (!e.getAttributeValue("conkeyref", "").equals("") && e.getAttributeValue("conaction", "").equals("")) { //$NON-NLS-6$ - emptyElement(e); - } - if (e.getAttributeValue("status", "").equals("removeContent")) { - e.setContent(new Vector<>()); - e.removeAttribute("status"); - } - List list = e.getChildren(); - for (int i = 0; i < list.size(); i++) { - cleanConref(list.get(i)); - } - } - - private void emptyElement(Element e) { - List children = e.getChildren(); - if (children.isEmpty()) { - e.setContent(new Vector<>()); - } else { - List content = e.getContent(); - Vector newContent = new Vector<>(); - Iterator it = content.iterator(); - while (it.hasNext()) { - XMLNode n = it.next(); - if (n.getNodeType() == XMLNode.ELEMENT_NODE) { - Element child = (Element) n; - emptyElement(child); - newContent.add(child); - } - } - e.setContent(newContent); - } - } - - private static void saveFile(Element element, String xliffFile) throws IOException { - Document doc = new Document(null, "xliff", null, null); - Element root = doc.getRootElement(); - root.setAttribute("version", "1.2"); - Element file = new Element("file"); - file.clone(element); - root.addContent(file); - List encoding = element.getPI("encoding"); - if (!encoding.isEmpty()) { - root.addContent(encoding.get(0)); - } - File xliff = File.createTempFile("tmp", ".xlf", new File(xliffFile).getParentFile()); - Element internal = file.getChild("header").getChild("skl").getChild("internal-file"); - if (internal != null) { - // embedded skeleton - File tmp = File.createTempFile("internal", ".skl", new File(xliffFile).getParentFile()); - tmp.deleteOnExit(); - if (internal.getAttributeValue("form", "").equals("base64")) { - Utils.decodeToFile(internal.getText(), tmp.getAbsolutePath()); - } else { - try (FileOutputStream out = new FileOutputStream(tmp)) { - List content = internal.getContent(); - for (int h = 0; h < content.size(); h++) { - XMLNode n = content.get(h); - if (n.getNodeType() == XMLNode.TEXT_NODE) { - out.write(StringConverter.toByteArray(((TextNode) n).getText())); - } else if (n.getNodeType() == XMLNode.CDATA_SECTION_NODE) { - out.write(((CData) n).getData().getBytes(StandardCharsets.UTF_8)); - } - } - } - } - file.getChild("header").getChild("skl").addContent(new Element("external-file")); - file.getChild("header").getChild("skl").getChild("external-file").setAttribute("href", - tmp.getAbsolutePath()); - file.getChild("header").getChild("skl").removeChild("internal-file"); - } - XMLOutputter outputter = new XMLOutputter(); - outputter.preserveSpace(true); - try (FileOutputStream output = new FileOutputStream(xliff.getAbsolutePath())) { - outputter.output(doc, output); - } - filesTable.put(element.getAttributeValue("original"), new String[] { xliff.getAbsolutePath(), - file.getChild("header").getChild("skl").getChild("external-file").getAttributeValue("href") }); - } - -} +/******************************************************************************* + * Copyright (c) 2003-2019 Maxprograms. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/org/documents/epl-v10.html + * + * Contributors: + * Maxprograms - initial API and implementation + *******************************************************************************/ +package com.maxprograms.converters.ditamap; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Vector; +import java.lang.System.Logger.Level; +import java.lang.System.Logger; + +import javax.xml.parsers.ParserConfigurationException; + +import org.xml.sax.SAXException; + +import com.maxprograms.converters.Constants; +import com.maxprograms.converters.StringConverter; +import com.maxprograms.converters.Utils; +import com.maxprograms.converters.xml.Xliff2Xml; +import com.maxprograms.xml.CData; +import com.maxprograms.xml.Catalog; +import com.maxprograms.xml.Document; +import com.maxprograms.xml.Element; +import com.maxprograms.xml.Indenter; +import com.maxprograms.xml.PI; +import com.maxprograms.xml.SAXBuilder; +import com.maxprograms.xml.TextNode; +import com.maxprograms.xml.XMLNode; +import com.maxprograms.xml.XMLOutputter; + +public class Xliff2DitaMap { + + private static Hashtable filesTable; + + public static Vector run(Hashtable params) { + Vector result = new Vector<>(); + String xliffFile = ""; + Xliff2DitaMap instance = new Xliff2DitaMap(); + try { + xliffFile = params.get("xliff"); + String outputFile = params.get("backfile"); + filesTable = new Hashtable<>(); + String catalog = params.get("catalog"); + SAXBuilder builder = new SAXBuilder(); + builder.preserveCustomAttributes(true); + builder.setEntityResolver(new Catalog(catalog)); + Document doc = builder.build(xliffFile); + Element root = doc.getRootElement(); + List files = root.getChildren("file"); + Iterator it = files.iterator(); + while (it.hasNext()) { + saveFile(it.next(), xliffFile); + } + String tgtlang = files.get(0).getAttributeValue("target-language", + files.get(0).getAttributeValue("source-language")); + Enumeration keys = filesTable.keys(); + XMLOutputter outputter = new XMLOutputter(); + outputter.preserveSpace(true); + while (keys.hasMoreElements()) { + String topicFile = keys.nextElement(); + String[] values = filesTable.get(topicFile); + Hashtable params2 = new Hashtable<>(); + params2.put("xliff", values[0]); + params2.put("skeleton", values[1]); + File folder = new File(outputFile); + File p = folder.getParentFile(); + if (p == null) { + p = new File(System.getProperty("user.dir")); + } + if (!p.exists()) { + p.mkdirs(); + } + params2.put("backfile", outputFile); + params2.put("encoding", params.get("encoding")); + params2.put("catalog", params.get("catalog")); + params2.put("dita_based", "yes"); + Vector res = Xliff2Xml.run(params2); + if (!Constants.SUCCESS.equals(res.get(0))) { + return res; + } + + doc = builder.build(outputFile); + Element r = doc.getRootElement(); + + List ish = doc.getPI("ish"); + String id = r.getAttributeValue("id", ""); + + if (!ish.isEmpty() || id.startsWith("GUID-")) { + restoreGUID(r); + } + + r.setAttribute("xml:lang", tgtlang); + Indenter.indent(r, 2); + instance.cleanConref(r); + try (FileOutputStream out = new FileOutputStream(outputFile)) { + outputter.output(doc, out); + } + File f = new File(values[0]); + Files.delete(Paths.get(f.toURI())); + } + + result.add(Constants.SUCCESS); + } catch (IOException | SAXException | ParserConfigurationException e) { + Logger logger = System.getLogger(Xliff2DitaMap.class.getName()); + logger.log(Level.ERROR, "Error merging DITA Map", e); + result.add(Constants.ERROR); + result.add(e.getMessage()); + } + return result; + } + + private static void restoreGUID(Element r) { + String href = r.getAttributeValue("href", ""); + if (!href.isEmpty()) { + int index = href.indexOf("GUID"); + if (index != -1) { + int end = href.indexOf('=', index + 1); + if (end != -1) { + String guid = href.substring(index, end); + r.setAttribute("href", guid); + } + } + } + String conref = r.getAttributeValue("conref", ""); + int index = conref.indexOf('='); + if (!conref.isEmpty() && conref.startsWith("GUID-") && index != -1) { + String guid = conref.substring(0, index); + index = conref.indexOf('#'); + if (index != -1) { + String rest = conref.substring(index); + r.setAttribute("conref", guid + rest); + } else { + r.setAttribute("conref", guid); + } + } + r.removeAttribute("class"); + r.removeAttribute("ditaarch:DITAArchVersion"); + r.removeAttribute("domains"); + List children = r.getChildren(); + for (int i = 0; i < children.size(); i++) { + restoreGUID(children.get(i)); + } + } + + private void cleanConref(Element e) { + if (!e.getAttributeValue("fluentaIgnore").isEmpty()) { + e.removeAttribute("fluentaIgnore"); + } + if (!e.getAttributeValue("keyref", "").equals("") + && e.getAttributeValue("status", "").equals("removeContent")) { //$NON-NLS-3$ + e.setContent(new Vector<>()); + e.removeAttribute("status"); + } + if (!e.getAttributeValue("conref", "").equals("") && e.getAttributeValue("conaction", "").equals("")) { //$NON-NLS-6$ + emptyElement(e); + } + if (!e.getAttributeValue("conkeyref", "").equals("") && e.getAttributeValue("conaction", "").equals("")) { //$NON-NLS-6$ + emptyElement(e); + } + if (e.getAttributeValue("status", "").equals("removeContent")) { + e.setContent(new Vector<>()); + e.removeAttribute("status"); + } + List list = e.getChildren(); + for (int i = 0; i < list.size(); i++) { + cleanConref(list.get(i)); + } + } + + private void emptyElement(Element e) { + List children = e.getChildren(); + if (children.isEmpty()) { + e.setContent(new Vector<>()); + } else { + List content = e.getContent(); + Vector newContent = new Vector<>(); + Iterator it = content.iterator(); + while (it.hasNext()) { + XMLNode n = it.next(); + if (n.getNodeType() == XMLNode.ELEMENT_NODE) { + Element child = (Element) n; + emptyElement(child); + newContent.add(child); + } + } + e.setContent(newContent); + } + } + + private static void saveFile(Element element, String xliffFile) throws IOException { + Document doc = new Document(null, "xliff", null, null); + Element root = doc.getRootElement(); + root.setAttribute("version", "1.2"); + Element file = new Element("file"); + file.clone(element); + root.addContent(file); + List encoding = element.getPI("encoding"); + if (!encoding.isEmpty()) { + root.addContent(encoding.get(0)); + } + File xliff = File.createTempFile("tmp", ".xlf", new File(xliffFile).getParentFile()); + Element internal = file.getChild("header").getChild("skl").getChild("internal-file"); + if (internal != null) { + // embedded skeleton + File tmp = File.createTempFile("internal", ".skl", new File(xliffFile).getParentFile()); + tmp.deleteOnExit(); + if (internal.getAttributeValue("form", "").equals("base64")) { + Utils.decodeToFile(internal.getText(), tmp.getAbsolutePath()); + } else { + try (FileOutputStream out = new FileOutputStream(tmp)) { + List content = internal.getContent(); + for (int h = 0; h < content.size(); h++) { + XMLNode n = content.get(h); + if (n.getNodeType() == XMLNode.TEXT_NODE) { + out.write(StringConverter.toByteArray(((TextNode) n).getText())); + } else if (n.getNodeType() == XMLNode.CDATA_SECTION_NODE) { + out.write(((CData) n).getData().getBytes(StandardCharsets.UTF_8)); + } + } + } + } + file.getChild("header").getChild("skl").addContent(new Element("external-file")); + file.getChild("header").getChild("skl").getChild("external-file").setAttribute("href", + tmp.getAbsolutePath()); + file.getChild("header").getChild("skl").removeChild("internal-file"); + } + XMLOutputter outputter = new XMLOutputter(); + outputter.preserveSpace(true); + try (FileOutputStream output = new FileOutputStream(xliff.getAbsolutePath())) { + outputter.output(doc, output); + } + filesTable.put(element.getAttributeValue("original"), new String[] { xliff.getAbsolutePath(), + file.getChild("header").getChild("skl").getChild("external-file").getAttributeValue("href") }); + } + +} diff --git a/src/com/maxprograms/converters/html/Html2Xliff.java b/src/com/maxprograms/converters/html/Html2Xliff.java index 4f5d9bc3..5ae68c2a 100644 --- a/src/com/maxprograms/converters/html/Html2Xliff.java +++ b/src/com/maxprograms/converters/html/Html2Xliff.java @@ -1,1055 +1,1056 @@ -/******************************************************************************* - * Copyright (c) 2003-2019 Maxprograms. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 1.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/org/documents/epl-v10.html - * - * Contributors: - * Maxprograms - initial API and implementation - *******************************************************************************/ -package com.maxprograms.converters.html; - -import java.io.ByteArrayInputStream; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.List; -import java.util.StringTokenizer; -import java.util.Vector; -import java.lang.System.Logger.Level; -import java.lang.System.Logger; - -import javax.xml.parsers.ParserConfigurationException; - -import org.xml.sax.SAXException; - -import com.maxprograms.converters.Utils; -import com.maxprograms.segmenter.Segmenter; -import com.maxprograms.xml.Catalog; -import com.maxprograms.xml.Document; -import com.maxprograms.xml.Element; -import com.maxprograms.xml.SAXBuilder; -import com.maxprograms.xml.TextNode; -import com.maxprograms.xml.XMLNode; - -public class Html2Xliff { - - private static String inputFile; - private static String skeletonFile; - private static String sourceLanguage; - private static String srcEncoding; - - private static FileOutputStream output; - private static FileOutputStream skeleton; - - private static int segId; - private static int tagId; - - private static List segments; - private static Hashtable startsSegment; - private static Hashtable> translatableAttributes; - private static Hashtable entities; - private static Hashtable ctypes; - private static Hashtable keepFormating; - - private static boolean segByElement; - private static boolean keepFormat; - - private static Segmenter segmenter; - private static String catalog; - private static String first; - private static String last; - private static String targetLanguage; - - private Html2Xliff() { - // do not instantiate this class - // use run method instead - } - - public static Vector run(Hashtable params) { - Vector result = new Vector<>(); - - inputFile = params.get("source"); - String xliffFile = params.get("xliff"); - skeletonFile = params.get("skeleton"); - sourceLanguage = params.get("srcLang"); - targetLanguage = params.get("tgtLang"); - srcEncoding = params.get("srcEncoding"); - String iniFile = params.get("iniFile"); - catalog = params.get("catalog"); - String paragraphSegmentation = params.get("paragraph"); - if (paragraphSegmentation == null) { - segByElement = false; - } else { - if (paragraphSegmentation.equals("yes")) { - segByElement = true; - } else { - segByElement = false; - } - } - try { - if (!segByElement) { - String initSegmenter = params.get("srxFile"); - segmenter = new Segmenter(initSegmenter, sourceLanguage, catalog); - } - try (FileInputStream input = new FileInputStream(inputFile)) { - skeleton = new FileOutputStream(skeletonFile); - output = new FileOutputStream(xliffFile); - writeHeader(); - - int size = input.available(); - byte[] array = new byte[size]; - if (size != input.read(array)) { - result.add("1"); - result.add("Error reading from input file."); - return result; - } - String file = new String(array, srcEncoding); - array = null; - buildTables(iniFile); - buildList(file); - file = null; - - processList(); - - skeleton.close(); - writeString("\n"); - writeString("\n"); - writeString(""); - output.close(); - } - result.add("0"); - } catch (IOException | SAXException | ParserConfigurationException e) { - Logger logger = System.getLogger(Html2Xliff.class.getName()); - logger.log(Level.ERROR, "Error converting HTML file", e); - result.add("1"); - result.add(e.getMessage()); - } - return result; - } - - private static void writeHeader() throws IOException { - - String tgtLang = ""; - if (targetLanguage != null) { - tgtLang = "\" target-language=\"" + targetLanguage; - } - writeString("\n"); - writeString("\n"); - writeString("\n"); - writeString("\n"); - - writeString("
\n"); - writeString(" \n"); - writeString(" \n"); - writeString(" \n"); - writeString("
\n"); - writeString("\n"); - } - - private static void processList() throws IOException, SAXException, ParserConfigurationException { - for (int i = 0; i < segments.size(); i++) { - String text = segments.get(i); - if (isTranslateable(text)) { - extractSegment(text); - } else { - // send directly to skeleton - writeSkeleton(text); - } - } - } - - private static void extractSegment(String seg) throws IOException, SAXException, ParserConfigurationException { - - // start by making a smaller list - - List miniList = new ArrayList<>(); - - int start = seg.indexOf('<'); - int end = seg.indexOf('>'); - String text = ""; - - while (start != -1) { - if (start > 0) { - // add initial text - miniList.add(seg.substring(0, start)); - seg = seg.substring(start); - start = seg.indexOf('<'); - end = seg.indexOf('>'); - } - // add the tag - if (end < seg.length()) { - miniList.add(seg.substring(start, end + 1)); - seg = seg.substring(end + 1); - start = seg.indexOf('<'); - end = seg.indexOf('>'); - } else { - miniList.add(seg); - start = -1; - } - } - if (seg.length() > 0) { - // add trailing characters - miniList.add(seg); - } - - int size = miniList.size(); - int i; - // separate initial text - String initial = ""; - for (i = 0; i < size; i++) { - text = miniList.get(i); - if (!isTranslateable(text)) { - initial = initial + text; - } else { - break; - } - } - - // get translatable - String translatable = text; - for (i++; i < size; i++) { - translatable = translatable + miniList.get(i); - } - - // get trailing text - String trail = ""; - int j; - for (j = size - 1; j > 0; j--) { - String t = miniList.get(j); - if (!isTranslateable(t)) { - trail = t + trail; - } else { - break; - } - } - - // remove trailing from translatable - start = translatable.lastIndexOf(trail); - if (start != -1) { - translatable = translatable.substring(0, start); - } - - writeSkeleton(initial); - String tagged = addTags(translatable); - if (containsText(tagged)) { - translatable = tagged; - if (segByElement) { - String[] frags = translatable.split("\u2029"); - for (int m = 0; m < frags.length; m++) { - writeSegment(frags[m]); - } - } else { - String[] frags = translatable.split("\u2029"); - for (int m = 0; m < frags.length; m++) { - String[] segs = segmenter.segment(frags[m]); - for (int h = 0; h < segs.length; h++) { - writeSegment(segs[h]); - } - } - } - } else { - writeSkeleton(translatable); - } - writeSkeleton(trail); - } - - private static void writeSegment(String segment) throws IOException, SAXException, ParserConfigurationException { - - segment = segment.replaceAll("\u2029", ""); - String pure = removePH(segment); - if (pure.trim().equals("")) { - writeSkeleton(phContent(segment)); - return; - } - if (segment.trim().equals("")) { - writeSkeleton(segment); - return; - } - - first = ""; - last = ""; - - segment = segmentCleanup(segment); - - writeSkeleton(first); - tagId = 0; - writeString(" \n" - + " "); - if (keepFormat) { - writeString(segment); - } else { - writeString(normalize(segment)); - } - writeString("\n \n"); - - writeSkeleton("%%%" + segId++ + "%%%\n" + last); - } - - private static String segmentCleanup(String segment) - throws SAXException, IOException, ParserConfigurationException { - ByteArrayInputStream stream = new ByteArrayInputStream( - ("" + segment + "").getBytes(StandardCharsets.UTF_8)); - SAXBuilder b = new SAXBuilder(); - Document d = b.build(stream); - Element e = d.getRootElement(); - List content = e.getContent(); - Iterator it = content.iterator(); - int count = 0; - while (it.hasNext()) { - XMLNode n = it.next(); - if (n.getNodeType() == XMLNode.ELEMENT_NODE) { - count++; - } - } - - if (count == 1) { - XMLNode n = content.get(0); - if (n.getNodeType() == XMLNode.ELEMENT_NODE) { - first = phContent(n.toString()); - content.remove(0); - } else { - n = content.get(content.size() - 1); - if (n.getNodeType() == XMLNode.ELEMENT_NODE) { - last = phContent(n.toString()); - content.remove(content.size() - 1); - } - } - } - - if (count == 2) { - XMLNode n = content.get(0); - XMLNode s = content.get(content.size() - 1); - if (n.getNodeType() == XMLNode.ELEMENT_NODE && s.getNodeType() == XMLNode.ELEMENT_NODE) { - first = ((Element) n).getText(); - content.remove(n); - last = ((Element) s).getText(); - content.remove(s); - } - } - e.setContent(content); - String es = e.toString(); - return es.substring(3, es.length() - 4); - } - - private static String phContent(String segment) throws SAXException, IOException, ParserConfigurationException { - ByteArrayInputStream stream = new ByteArrayInputStream( - ("" + segment + "").getBytes(StandardCharsets.UTF_8)); - SAXBuilder b = new SAXBuilder(); - Document d = b.build(stream); - Element e = d.getRootElement(); - List content = e.getContent(); - Iterator it = content.iterator(); - String result = ""; - while (it.hasNext()) { - XMLNode n = it.next(); - if (n.getNodeType() == XMLNode.ELEMENT_NODE) { - result = result + ((Element) n).getText(); - } - if (n.getNodeType() == XMLNode.TEXT_NODE) { - result = result + ((TextNode) n).getText(); - } - } - return result; - } - - private static String removePH(String segment) throws SAXException, IOException, ParserConfigurationException { - ByteArrayInputStream stream = new ByteArrayInputStream( - ("" + segment + "").getBytes(StandardCharsets.UTF_8)); - SAXBuilder b = new SAXBuilder(); - Document d = b.build(stream); - Element e = d.getRootElement(); - List content = e.getContent(); - Iterator it = content.iterator(); - String result = ""; - while (it.hasNext()) { - XMLNode n = it.next(); - if (n.getNodeType() == XMLNode.TEXT_NODE) { - result = result + ((TextNode) n).getText(); - } - } - return result; - } - - private static String normalize(String string) { - string = string.replace('\n', ' '); - string = string.replace('\t', ' '); - string = string.replace('\r', ' '); - string = string.replace('\f', ' '); - String rs = ""; - int length = string.length(); - for (int i = 0; i < length; i++) { - char ch = string.charAt(i); - if (ch != ' ') { - rs = rs + ch; - } else { - rs = rs + ch; - while (i < (length - 1) && string.charAt(i + 1) == ' ') { - i++; - } - } - } - return rs; - } - - private static String addTags(String src) { - String result = ""; - int start = src.indexOf('<'); - int end = src.indexOf('>'); - - while (start != -1) { - if (start > 0) { - result = result + cleanString(src.substring(0, start)); - src = src.substring(start); - start = src.indexOf('<'); - end = src.indexOf('>'); - } - String element = src.substring(start, end + 1); - // check if we don't fall in a trap - // from Adobe GoLive - int errors = element.indexOf('<', 1); - if (errors != -1) { - // there is a "<" inside quotes - // must move manually until the end - // of the element avoiding angle brackets - // within quotes - boolean exit = false; - boolean inQuotes = false; - StringBuilder buffer = new StringBuilder(); - buffer.append("<"); - int k = 0; - while (!exit) { - k++; - char c = src.charAt(start + k); - if (c == '\"') { - inQuotes = !inQuotes; - } - buffer.append(c); - if (!inQuotes && c == '>') { - exit = true; - } - } - element = buffer.toString(); - end = start + buffer.toString().length(); - } - - String tagged = tag(element); - result = result + tagged; - - src = src.substring(end + 1); - - start = src.indexOf('<'); - end = src.indexOf('>'); - } - result = result + cleanString(src); - return result; - } - - private static String tag(String element) { - String result = ""; - String type = getType(element); - if (translatableAttributes.containsKey(type)) { - result = extractAttributes(type, element); - if (result.indexOf("\u2029") == -1) { - String ctype = ""; - if (ctypes.containsKey(type)) { - ctype = " ctype=\"" + ctypes.get(type) + "\""; - } - result = "" + cleanString(element) + ""; - } - } else { - String ctype = ""; - if (ctypes.containsKey(type)) { - ctype = " ctype=\"" + ctypes.get(type) + "\""; - } - result = "" + cleanString(element) + ""; - } - return result; - } - - private static String cleanString(String s) { - int control = s.indexOf('&'); - while (control != -1) { - int sc = s.indexOf(';', control); - if (sc == -1) { - // no semicolon, it's not an entity - s = s.substring(0, control) + "&" + s.substring(control + 1); - } else { - // may be an entity - String candidate = s.substring(control, sc) + ";"; - if (!(candidate.equals("&") || candidate.equals(">") || candidate.equals("<"))) { - String entity = entities.get(candidate); - if (entity != null) { - s = s.substring(0, control) + entity + s.substring(sc + 1); - } else { - if (candidate.startsWith("&#x")) { - // numeric entity in hex, get its value - int ch = Integer.parseInt(candidate.substring(3, candidate.indexOf(';')), 16); - s = s.substring(0, control) + (char) ch + s.substring(sc + 1); - } else if (candidate.startsWith("&#")) { - // numeric entity in decimal - int ch = Integer.parseInt(candidate.substring(2, candidate.indexOf(';'))); - s = s.substring(0, control) + (char) ch + s.substring(sc + 1); - } else { - // ugly, this should never happen - s = s.substring(0, control) + "%%%ph id=\"" + tagId++ + "\"%%%&" - + candidate.substring(1) + "%%%/ph%%%" + s.substring(sc + 1); - } - } - } - } - if (control < s.length()) { - control++; - } - control = s.indexOf('&', control); - } - - control = s.indexOf('<'); - while (control != -1) { - s = s.substring(0, control) + "<" + s.substring(control + 1); - if (control < s.length()) { - control++; - } - control = s.indexOf('<', control); - } - - control = s.indexOf('>'); - while (control != -1) { - s = s.substring(0, control) + ">" + s.substring(control + 1); - if (control < s.length()) { - control++; - } - control = s.indexOf('>', control); - } - s = s.replaceAll("%%%/ph%%%", ""); - s = s.replaceAll("%%%ph", "&"); - return s; - } - - private static void writeSkeleton(String string) throws IOException { - skeleton.write(string.getBytes(StandardCharsets.UTF_8)); - } - - private static void writeString(String string) throws IOException { - output.write(string.getBytes(StandardCharsets.UTF_8)); - } - - private static boolean isTranslateable(String string) { - - keepFormat = false; - - // - // remove CDATA sections - // - int startComment = string.indexOf(""); - while (startComment != -1 || endComment != -1) { - if (startComment != -1) { - if (endComment == -1) { - string = string.substring(0, startComment); - } else { - if (startComment < endComment) { - string = string.substring(0, startComment) + string.substring(endComment + 3); - } else { - string = string.substring(endComment + 3, startComment); - } - } - } else { - if (endComment != -1) { - string = string.substring(endComment + 3); - } - } - startComment = string.indexOf(""); - } - // - // remove IGNORE sections - // - startComment = string.indexOf(""); - while (startComment != -1 || endComment != -1) { - if (startComment != -1) { - if (endComment == -1) { - string = string.substring(0, startComment); - } else { - if (startComment < endComment) { - string = string.substring(0, startComment) + string.substring(endComment + 3); - } else { - string = string.substring(endComment + 3, startComment); - } - } - } else { - if (endComment != -1) { - string = string.substring(endComment + 3); - } - } - startComment = string.indexOf(""); - } - // - // remove IGNORE sections - // - startComment = string.indexOf(""); - while (startComment != -1 || endComment != -1) { - if (startComment != -1) { - if (endComment == -1) { - string = string.substring(0, startComment); - } else { - if (startComment < endComment) { - string = string.substring(0, startComment) + string.substring(endComment + 3); - } else { - string = string.substring(endComment + 3, startComment); - } - } - } else { - if (endComment != -1) { - string = string.substring(endComment + 3); - } - } - startComment = string.indexOf(""); - } - // - // remove XML style comments - // - startComment = string.indexOf(""); - while (startComment != -1 || endComment != -1) { - if (startComment != -1) { - if (endComment == -1) { - string = string.substring(0, startComment); - } else { - if (startComment < endComment) { - string = string.substring(0, startComment) + string.substring(endComment + 3); - } else { - string = string.substring(endComment + 3, startComment); - } - } - } else { - if (endComment != -1) { - string = string.substring(endComment + 3); - } - } - startComment = string.indexOf(""); - } - // - // remove Processing Instruction - // - startComment = string.indexOf(""); - while (startComment != -1 || endComment != -1) { - if (startComment != -1) { - if (endComment == -1) { - string = string.substring(0, startComment); - } else { - string = string.substring(0, startComment) + string.substring(endComment + 2); - } - } else { - if (endComment != -1) { - string = string.substring(endComment + 2); - } - } - startComment = string.indexOf(""); - } - // - // remove C style comments - // - startComment = string.indexOf("/*"); - endComment = string.indexOf("*/"); - while (startComment != -1 || endComment != -1) { - if (startComment != -1) { - if (endComment == -1) { - string = string.substring(0, startComment); - } else { - if (startComment < endComment) { - string = string.substring(0, startComment) + string.substring(endComment + 2); - } else { - string = string.substring(endComment + 2, startComment); - } - } - } else { - if (endComment != -1) { - string = string.substring(endComment + 2); - } - } - startComment = string.indexOf("/*"); - endComment = string.indexOf("*/"); - } - // - // Start checking - // - int start = string.indexOf('<'); - int end = string.indexOf('>'); - - String type; - String text = ""; - - while (start != -1) { - if (start > 0) { - text = text + cleanString(string.substring(0, start)); - string = string.substring(start); - start = string.indexOf('<'); - end = string.indexOf('>'); - } - type = getType(string.substring(start, end)); - keepFormat = keepFormating.containsKey(type); - - if (type.equals("script") || type.equals("style")) { - return false; - } - - // check for translatable attributes - if (translatableAttributes.containsKey(type)) { - return true; - } - if (type.startsWith("/") && translatableAttributes.containsKey(type.substring(1))) { - return true; - } - if (end < string.length()) { - string = string.substring(end + 1); - } else { - string = string.substring(end); - } - start = string.indexOf('<'); - end = string.indexOf('>'); - } - - text = text + cleanString(string); - - // look for non-white space - - for (int i = 0; i < text.length(); i++) { - char c = text.charAt(i); - if (c == ' ' || c == '\n' || c == '\f' || c == '\t' || c == '\r' || c == '\u00A0' || c == '\u2007' - || c == '\u202F' || c == '\uFEFF' || c == '>') { - continue; - } - return true; - } - - return false; - } - - private static String extractAttributes(String type, String element) { - - String ctype = ""; - if (ctypes.containsKey(type)) { - ctype = " ctype=\"" + ctypes.get(type) + "\""; - } - String result = ""; - element = cleanString(element); - - Vector v = translatableAttributes.get(type); - - StringTokenizer tokenizer = new StringTokenizer(element, "&= \t\n\r\f/", true); - while (tokenizer.hasMoreTokens()) { - String token = tokenizer.nextToken(); - if (!v.contains(token.toLowerCase())) { - result = result + token; - } else { - result = result + token; - String s = tokenizer.nextToken(); - while (s.equals("=") || s.equals(" ")) { - result = result + s; - s = tokenizer.nextToken(); - } - // s contains the first word of the atttribute - if ((s.startsWith("\"") && s.endsWith("\"") || s.startsWith("'") && s.endsWith("'")) - && s.length() > 1) { - // the value is one word and it is quoted - result = result + s.substring(0, 1) + "\u2029" + s.substring(1, s.length() - 1) - + "\u2029" + s.substring(s.length() - 1); - } else { - if (s.startsWith("\"") || s.startsWith("'")) { - // attribute value is quoted, but it has more than one word - String quote = s.substring(0, 1); - result = result + s.substring(0, 1) + "\u2029" + s.substring(1); - s = tokenizer.nextToken(); - do { - result = result + s; - s = tokenizer.nextToken(); - } while (s.indexOf(quote) == -1); - String left = s.substring(0, s.indexOf(quote)); - String right = s.substring(s.indexOf(quote)); - result = result + left + "\u2029" + right; - } else { - // attribute is not quoted, it can only be one word - result = result + "\u2029" + s + "\u2029"; - } - } - } - } - result = result + ""; - return result; - } - - private static void buildTables(String iniFile) throws SAXException, IOException, ParserConfigurationException { - - SAXBuilder builder = new SAXBuilder(); - Catalog cat = new Catalog(catalog); - builder.setEntityResolver(cat); - Document doc = builder.build(iniFile); - Element root = doc.getRootElement(); - List tags = root.getChildren("tag"); - - startsSegment = new Hashtable<>(); - translatableAttributes = new Hashtable<>(); - entities = new Hashtable<>(); - ctypes = new Hashtable<>(); - keepFormating = new Hashtable<>(); - - Iterator i = tags.iterator(); - while (i.hasNext()) { - Element t = i.next(); - if (t.getAttributeValue("hard-break", "inline").equals("segment")) { - startsSegment.put(t.getText(), "yes"); - } - if (t.getAttributeValue("keep-format", "no").equals("yes")) { - keepFormating.put(t.getText(), "yes"); - } - String attributes = t.getAttributeValue("attributes", ""); - if (!attributes.equals("")) { - StringTokenizer tokenizer = new StringTokenizer(attributes, ";"); - int count = tokenizer.countTokens(); - Vector v = new Vector<>(count); - for (int j = 0; j < count; j++) { - v.add(tokenizer.nextToken()); - } - translatableAttributes.put(t.getText(), v); - } - String ctype = t.getAttributeValue("ctype", ""); - if (!ctype.equals("")) { - ctypes.put(t.getText(), ctype); - } - } - - List ents = root.getChildren("entity"); - Iterator it = ents.iterator(); - while (it.hasNext()) { - Element e = it.next(); - entities.put("&" + e.getAttributeValue("name") + ";", e.getText()); - } - } - - private static void buildList(String file) throws IOException { - segments = new ArrayList<>(); - int start = file.indexOf('<'); - int end = file.indexOf('>'); - String type; - String text = ""; - - if (start > 0) { - segments.add(file.substring(0, start)); - file = file.substring(start); - start = file.indexOf('<'); - end = file.indexOf('>'); - } - while (start != -1) { - if (file.substring(start, start + 3).equals("") + 2; - } - if (end < start || end < 0 || start < 0) { - throw new IOException("nvalid HTML markup found."); - } - String element = file.substring(start, end + 1); - - // check if the element is OK or has strange stuff - // from Adobe GoLive 5 - int errors = element.indexOf('<', 1); - if (errors != -1 && !element.startsWith("') { - exit = true; - } - } - element = buffer.toString(); - end = start + buffer.toString().length(); - start = end; - } - - type = getType(element); - - if (type.equals("![INCLUDE[") || type.equals("![IGNORE[") || type.equals("![CDATA[")) { - // it's an SGML section, send it to skeleton - // and keep looking for segments - segments.add(text); - text = ""; - end = file.indexOf("]]>"); - segments.add(file.substring(start, end + 3)); - file = file.substring(end + 3); - start = file.indexOf('<'); - end = file.indexOf('>'); - if (start != -1) { - text = file.substring(0, start); - } - continue; - } - if (startsSegment.containsKey(type)) { - segments.add(text); - file = file.substring(text.length()); - start = start - text.length(); - end = end - text.length(); - text = ""; - } - if (type.startsWith("/") && startsSegment.containsKey(type.substring(1))) { - segments.add(text); - file = file.substring(text.length()); - start = start - text.length(); - end = end - text.length(); - text = ""; - } - if (type.equals("script") || type.equals("style")) { - // send the whole element to the list - segments.add(text); - file = file.substring(text.length()); - text = ""; - if (!element.endsWith("/>")) { - end = file.toLowerCase().indexOf("/" + type + ">"); - String script = file.substring(0, end + 2 + type.length()); - if (script.indexOf("") == -1) { - // there are nested scripts inside this one - end = file.toLowerCase().indexOf("/" + type + ">", file.indexOf("-->") + 3); - script = file.substring(0, end + 2 + type.length()); - } - segments.add(script); - file = file.substring(end + 2 + type.length()); - } else { - segments.add(element); - file = file.substring(element.length()); - } - - start = file.indexOf('<'); - end = file.indexOf('>'); - if (start != -1) { - text = file.substring(0, start); - } - continue; - } - if (type.equals("!--")) { - // it's a comment, send it to skeleton - // and keep looking for segments - segments.add(text); - file = file.substring(text.length()); - text = ""; - end = file.indexOf("-->"); - String comment = file.substring(0, end + 3); - segments.add(comment); - file = file.substring(end + 3); - start = file.indexOf('<'); - end = file.indexOf('>'); - if (start != -1) { - text = file.substring(0, start); - } - continue; - } - - text = text + element; - - if (end < file.length()) { // there may be text to extract - int nextTag = file.indexOf('<', end); - if (nextTag != -1) { - text += file.substring(end + 1, nextTag); - } - } - - if (start < file.length()) { - start++; - } - if (end < file.length()) { - end = file.indexOf('>', end + 1); - } else { - end = file.length(); - } - start = file.indexOf('<', start); - } - segments.add(text); - } - - private static String getType(String string) { - - String result = ""; - if (string.startsWith("') { - break; - } - result = result + c; - } - if (result.endsWith("/") && result.length() > 1) { - result = result.substring(0, result.length() - 1); - } - return result.toLowerCase(); - } - - private static boolean containsText(String string) { - StringBuilder buffer = new StringBuilder(); - int length = string.length(); - boolean inTag = false; - - for (int i = 0; i < length; i++) { - char c = string.charAt(i); - if (string.substring(i).startsWith("")) { - inTag = false; - i = i + 4; - continue; - } - if (!inTag) { - buffer.append(c); - } - } - return !buffer.toString().trim().equals(""); - } - -} +/******************************************************************************* + * Copyright (c) 2003-2019 Maxprograms. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/org/documents/epl-v10.html + * + * Contributors: + * Maxprograms - initial API and implementation + *******************************************************************************/ +package com.maxprograms.converters.html; + +import java.io.ByteArrayInputStream; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.StringTokenizer; +import java.util.Vector; +import java.lang.System.Logger.Level; +import java.lang.System.Logger; + +import javax.xml.parsers.ParserConfigurationException; + +import org.xml.sax.SAXException; + +import com.maxprograms.converters.Constants; +import com.maxprograms.converters.Utils; +import com.maxprograms.segmenter.Segmenter; +import com.maxprograms.xml.Catalog; +import com.maxprograms.xml.Document; +import com.maxprograms.xml.Element; +import com.maxprograms.xml.SAXBuilder; +import com.maxprograms.xml.TextNode; +import com.maxprograms.xml.XMLNode; + +public class Html2Xliff { + + private static String inputFile; + private static String skeletonFile; + private static String sourceLanguage; + private static String srcEncoding; + + private static FileOutputStream output; + private static FileOutputStream skeleton; + + private static int segId; + private static int tagId; + + private static List segments; + private static Hashtable startsSegment; + private static Hashtable> translatableAttributes; + private static Hashtable entities; + private static Hashtable ctypes; + private static Hashtable keepFormating; + + private static boolean segByElement; + private static boolean keepFormat; + + private static Segmenter segmenter; + private static String catalog; + private static String first; + private static String last; + private static String targetLanguage; + + private Html2Xliff() { + // do not instantiate this class + // use run method instead + } + + public static Vector run(Hashtable params) { + Vector result = new Vector<>(); + + inputFile = params.get("source"); + String xliffFile = params.get("xliff"); + skeletonFile = params.get("skeleton"); + sourceLanguage = params.get("srcLang"); + targetLanguage = params.get("tgtLang"); + srcEncoding = params.get("srcEncoding"); + String iniFile = params.get("iniFile"); + catalog = params.get("catalog"); + String paragraphSegmentation = params.get("paragraph"); + if (paragraphSegmentation == null) { + segByElement = false; + } else { + if (paragraphSegmentation.equals("yes")) { + segByElement = true; + } else { + segByElement = false; + } + } + try { + if (!segByElement) { + String initSegmenter = params.get("srxFile"); + segmenter = new Segmenter(initSegmenter, sourceLanguage, catalog); + } + try (FileInputStream input = new FileInputStream(inputFile)) { + skeleton = new FileOutputStream(skeletonFile); + output = new FileOutputStream(xliffFile); + writeHeader(); + + int size = input.available(); + byte[] array = new byte[size]; + if (size != input.read(array)) { + result.add(Constants.ERROR); + result.add("Error reading from input file."); + return result; + } + String file = new String(array, srcEncoding); + array = null; + buildTables(iniFile); + buildList(file); + file = null; + + processList(); + + skeleton.close(); + writeString("\n"); + writeString("
\n"); + writeString("
"); + output.close(); + } + result.add(Constants.SUCCESS); + } catch (IOException | SAXException | ParserConfigurationException e) { + Logger logger = System.getLogger(Html2Xliff.class.getName()); + logger.log(Level.ERROR, "Error converting HTML file", e); + result.add(Constants.ERROR); + result.add(e.getMessage()); + } + return result; + } + + private static void writeHeader() throws IOException { + + String tgtLang = ""; + if (targetLanguage != null) { + tgtLang = "\" target-language=\"" + targetLanguage; + } + writeString("\n"); + writeString("\n"); + writeString("\n"); + writeString("\n"); + + writeString("
\n"); + writeString(" \n"); + writeString(" \n"); + writeString(" \n"); + writeString("
\n"); + writeString("\n"); + } + + private static void processList() throws IOException, SAXException, ParserConfigurationException { + for (int i = 0; i < segments.size(); i++) { + String text = segments.get(i); + if (isTranslateable(text)) { + extractSegment(text); + } else { + // send directly to skeleton + writeSkeleton(text); + } + } + } + + private static void extractSegment(String seg) throws IOException, SAXException, ParserConfigurationException { + + // start by making a smaller list + + List miniList = new ArrayList<>(); + + int start = seg.indexOf('<'); + int end = seg.indexOf('>'); + String text = ""; + + while (start != -1) { + if (start > 0) { + // add initial text + miniList.add(seg.substring(0, start)); + seg = seg.substring(start); + start = seg.indexOf('<'); + end = seg.indexOf('>'); + } + // add the tag + if (end < seg.length()) { + miniList.add(seg.substring(start, end + 1)); + seg = seg.substring(end + 1); + start = seg.indexOf('<'); + end = seg.indexOf('>'); + } else { + miniList.add(seg); + start = -1; + } + } + if (seg.length() > 0) { + // add trailing characters + miniList.add(seg); + } + + int size = miniList.size(); + int i; + // separate initial text + String initial = ""; + for (i = 0; i < size; i++) { + text = miniList.get(i); + if (!isTranslateable(text)) { + initial = initial + text; + } else { + break; + } + } + + // get translatable + String translatable = text; + for (i++; i < size; i++) { + translatable = translatable + miniList.get(i); + } + + // get trailing text + String trail = ""; + int j; + for (j = size - 1; j > 0; j--) { + String t = miniList.get(j); + if (!isTranslateable(t)) { + trail = t + trail; + } else { + break; + } + } + + // remove trailing from translatable + start = translatable.lastIndexOf(trail); + if (start != -1) { + translatable = translatable.substring(0, start); + } + + writeSkeleton(initial); + String tagged = addTags(translatable); + if (containsText(tagged)) { + translatable = tagged; + if (segByElement) { + String[] frags = translatable.split("\u2029"); + for (int m = 0; m < frags.length; m++) { + writeSegment(frags[m]); + } + } else { + String[] frags = translatable.split("\u2029"); + for (int m = 0; m < frags.length; m++) { + String[] segs = segmenter.segment(frags[m]); + for (int h = 0; h < segs.length; h++) { + writeSegment(segs[h]); + } + } + } + } else { + writeSkeleton(translatable); + } + writeSkeleton(trail); + } + + private static void writeSegment(String segment) throws IOException, SAXException, ParserConfigurationException { + + segment = segment.replaceAll("\u2029", ""); + String pure = removePH(segment); + if (pure.trim().equals("")) { + writeSkeleton(phContent(segment)); + return; + } + if (segment.trim().equals("")) { + writeSkeleton(segment); + return; + } + + first = ""; + last = ""; + + segment = segmentCleanup(segment); + + writeSkeleton(first); + tagId = 0; + writeString(" \n" + + " "); + if (keepFormat) { + writeString(segment); + } else { + writeString(normalize(segment)); + } + writeString("\n \n"); + + writeSkeleton("%%%" + segId++ + "%%%\n" + last); + } + + private static String segmentCleanup(String segment) + throws SAXException, IOException, ParserConfigurationException { + ByteArrayInputStream stream = new ByteArrayInputStream( + ("" + segment + "").getBytes(StandardCharsets.UTF_8)); + SAXBuilder b = new SAXBuilder(); + Document d = b.build(stream); + Element e = d.getRootElement(); + List content = e.getContent(); + Iterator it = content.iterator(); + int count = 0; + while (it.hasNext()) { + XMLNode n = it.next(); + if (n.getNodeType() == XMLNode.ELEMENT_NODE) { + count++; + } + } + + if (count == 1) { + XMLNode n = content.get(0); + if (n.getNodeType() == XMLNode.ELEMENT_NODE) { + first = phContent(n.toString()); + content.remove(0); + } else { + n = content.get(content.size() - 1); + if (n.getNodeType() == XMLNode.ELEMENT_NODE) { + last = phContent(n.toString()); + content.remove(content.size() - 1); + } + } + } + + if (count == 2) { + XMLNode n = content.get(0); + XMLNode s = content.get(content.size() - 1); + if (n.getNodeType() == XMLNode.ELEMENT_NODE && s.getNodeType() == XMLNode.ELEMENT_NODE) { + first = ((Element) n).getText(); + content.remove(n); + last = ((Element) s).getText(); + content.remove(s); + } + } + e.setContent(content); + String es = e.toString(); + return es.substring(3, es.length() - 4); + } + + private static String phContent(String segment) throws SAXException, IOException, ParserConfigurationException { + ByteArrayInputStream stream = new ByteArrayInputStream( + ("" + segment + "").getBytes(StandardCharsets.UTF_8)); + SAXBuilder b = new SAXBuilder(); + Document d = b.build(stream); + Element e = d.getRootElement(); + List content = e.getContent(); + Iterator it = content.iterator(); + String result = ""; + while (it.hasNext()) { + XMLNode n = it.next(); + if (n.getNodeType() == XMLNode.ELEMENT_NODE) { + result = result + ((Element) n).getText(); + } + if (n.getNodeType() == XMLNode.TEXT_NODE) { + result = result + ((TextNode) n).getText(); + } + } + return result; + } + + private static String removePH(String segment) throws SAXException, IOException, ParserConfigurationException { + ByteArrayInputStream stream = new ByteArrayInputStream( + ("" + segment + "").getBytes(StandardCharsets.UTF_8)); + SAXBuilder b = new SAXBuilder(); + Document d = b.build(stream); + Element e = d.getRootElement(); + List content = e.getContent(); + Iterator it = content.iterator(); + String result = ""; + while (it.hasNext()) { + XMLNode n = it.next(); + if (n.getNodeType() == XMLNode.TEXT_NODE) { + result = result + ((TextNode) n).getText(); + } + } + return result; + } + + private static String normalize(String string) { + string = string.replace('\n', ' '); + string = string.replace('\t', ' '); + string = string.replace('\r', ' '); + string = string.replace('\f', ' '); + String rs = ""; + int length = string.length(); + for (int i = 0; i < length; i++) { + char ch = string.charAt(i); + if (ch != ' ') { + rs = rs + ch; + } else { + rs = rs + ch; + while (i < (length - 1) && string.charAt(i + 1) == ' ') { + i++; + } + } + } + return rs; + } + + private static String addTags(String src) { + String result = ""; + int start = src.indexOf('<'); + int end = src.indexOf('>'); + + while (start != -1) { + if (start > 0) { + result = result + cleanString(src.substring(0, start)); + src = src.substring(start); + start = src.indexOf('<'); + end = src.indexOf('>'); + } + String element = src.substring(start, end + 1); + // check if we don't fall in a trap + // from Adobe GoLive + int errors = element.indexOf('<', 1); + if (errors != -1) { + // there is a "<" inside quotes + // must move manually until the end + // of the element avoiding angle brackets + // within quotes + boolean exit = false; + boolean inQuotes = false; + StringBuilder buffer = new StringBuilder(); + buffer.append("<"); + int k = 0; + while (!exit) { + k++; + char c = src.charAt(start + k); + if (c == '\"') { + inQuotes = !inQuotes; + } + buffer.append(c); + if (!inQuotes && c == '>') { + exit = true; + } + } + element = buffer.toString(); + end = start + buffer.toString().length(); + } + + String tagged = tag(element); + result = result + tagged; + + src = src.substring(end + 1); + + start = src.indexOf('<'); + end = src.indexOf('>'); + } + result = result + cleanString(src); + return result; + } + + private static String tag(String element) { + String result = ""; + String type = getType(element); + if (translatableAttributes.containsKey(type)) { + result = extractAttributes(type, element); + if (result.indexOf("\u2029") == -1) { + String ctype = ""; + if (ctypes.containsKey(type)) { + ctype = " ctype=\"" + ctypes.get(type) + "\""; + } + result = "" + cleanString(element) + ""; + } + } else { + String ctype = ""; + if (ctypes.containsKey(type)) { + ctype = " ctype=\"" + ctypes.get(type) + "\""; + } + result = "" + cleanString(element) + ""; + } + return result; + } + + private static String cleanString(String s) { + int control = s.indexOf('&'); + while (control != -1) { + int sc = s.indexOf(';', control); + if (sc == -1) { + // no semicolon, it's not an entity + s = s.substring(0, control) + "&" + s.substring(control + 1); + } else { + // may be an entity + String candidate = s.substring(control, sc) + ";"; + if (!(candidate.equals("&") || candidate.equals(">") || candidate.equals("<"))) { + String entity = entities.get(candidate); + if (entity != null) { + s = s.substring(0, control) + entity + s.substring(sc + 1); + } else { + if (candidate.startsWith("&#x")) { + // numeric entity in hex, get its value + int ch = Integer.parseInt(candidate.substring(3, candidate.indexOf(';')), 16); + s = s.substring(0, control) + (char) ch + s.substring(sc + 1); + } else if (candidate.startsWith("&#")) { + // numeric entity in decimal + int ch = Integer.parseInt(candidate.substring(2, candidate.indexOf(';'))); + s = s.substring(0, control) + (char) ch + s.substring(sc + 1); + } else { + // ugly, this should never happen + s = s.substring(0, control) + "%%%ph id=\"" + tagId++ + "\"%%%&" + + candidate.substring(1) + "%%%/ph%%%" + s.substring(sc + 1); + } + } + } + } + if (control < s.length()) { + control++; + } + control = s.indexOf('&', control); + } + + control = s.indexOf('<'); + while (control != -1) { + s = s.substring(0, control) + "<" + s.substring(control + 1); + if (control < s.length()) { + control++; + } + control = s.indexOf('<', control); + } + + control = s.indexOf('>'); + while (control != -1) { + s = s.substring(0, control) + ">" + s.substring(control + 1); + if (control < s.length()) { + control++; + } + control = s.indexOf('>', control); + } + s = s.replaceAll("%%%/ph%%%", ""); + s = s.replaceAll("%%%ph", "&"); + return s; + } + + private static void writeSkeleton(String string) throws IOException { + skeleton.write(string.getBytes(StandardCharsets.UTF_8)); + } + + private static void writeString(String string) throws IOException { + output.write(string.getBytes(StandardCharsets.UTF_8)); + } + + private static boolean isTranslateable(String string) { + + keepFormat = false; + + // + // remove CDATA sections + // + int startComment = string.indexOf(""); + while (startComment != -1 || endComment != -1) { + if (startComment != -1) { + if (endComment == -1) { + string = string.substring(0, startComment); + } else { + if (startComment < endComment) { + string = string.substring(0, startComment) + string.substring(endComment + 3); + } else { + string = string.substring(endComment + 3, startComment); + } + } + } else { + if (endComment != -1) { + string = string.substring(endComment + 3); + } + } + startComment = string.indexOf(""); + } + // + // remove IGNORE sections + // + startComment = string.indexOf(""); + while (startComment != -1 || endComment != -1) { + if (startComment != -1) { + if (endComment == -1) { + string = string.substring(0, startComment); + } else { + if (startComment < endComment) { + string = string.substring(0, startComment) + string.substring(endComment + 3); + } else { + string = string.substring(endComment + 3, startComment); + } + } + } else { + if (endComment != -1) { + string = string.substring(endComment + 3); + } + } + startComment = string.indexOf(""); + } + // + // remove IGNORE sections + // + startComment = string.indexOf(""); + while (startComment != -1 || endComment != -1) { + if (startComment != -1) { + if (endComment == -1) { + string = string.substring(0, startComment); + } else { + if (startComment < endComment) { + string = string.substring(0, startComment) + string.substring(endComment + 3); + } else { + string = string.substring(endComment + 3, startComment); + } + } + } else { + if (endComment != -1) { + string = string.substring(endComment + 3); + } + } + startComment = string.indexOf(""); + } + // + // remove XML style comments + // + startComment = string.indexOf(""); + while (startComment != -1 || endComment != -1) { + if (startComment != -1) { + if (endComment == -1) { + string = string.substring(0, startComment); + } else { + if (startComment < endComment) { + string = string.substring(0, startComment) + string.substring(endComment + 3); + } else { + string = string.substring(endComment + 3, startComment); + } + } + } else { + if (endComment != -1) { + string = string.substring(endComment + 3); + } + } + startComment = string.indexOf(""); + } + // + // remove Processing Instruction + // + startComment = string.indexOf(""); + while (startComment != -1 || endComment != -1) { + if (startComment != -1) { + if (endComment == -1) { + string = string.substring(0, startComment); + } else { + string = string.substring(0, startComment) + string.substring(endComment + 2); + } + } else { + if (endComment != -1) { + string = string.substring(endComment + 2); + } + } + startComment = string.indexOf(""); + } + // + // remove C style comments + // + startComment = string.indexOf("/*"); + endComment = string.indexOf("*/"); + while (startComment != -1 || endComment != -1) { + if (startComment != -1) { + if (endComment == -1) { + string = string.substring(0, startComment); + } else { + if (startComment < endComment) { + string = string.substring(0, startComment) + string.substring(endComment + 2); + } else { + string = string.substring(endComment + 2, startComment); + } + } + } else { + if (endComment != -1) { + string = string.substring(endComment + 2); + } + } + startComment = string.indexOf("/*"); + endComment = string.indexOf("*/"); + } + // + // Start checking + // + int start = string.indexOf('<'); + int end = string.indexOf('>'); + + String type; + String text = ""; + + while (start != -1) { + if (start > 0) { + text = text + cleanString(string.substring(0, start)); + string = string.substring(start); + start = string.indexOf('<'); + end = string.indexOf('>'); + } + type = getType(string.substring(start, end)); + keepFormat = keepFormating.containsKey(type); + + if (type.equals("script") || type.equals("style")) { + return false; + } + + // check for translatable attributes + if (translatableAttributes.containsKey(type)) { + return true; + } + if (type.startsWith("/") && translatableAttributes.containsKey(type.substring(1))) { + return true; + } + if (end < string.length()) { + string = string.substring(end + 1); + } else { + string = string.substring(end); + } + start = string.indexOf('<'); + end = string.indexOf('>'); + } + + text = text + cleanString(string); + + // look for non-white space + + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (c == ' ' || c == '\n' || c == '\f' || c == '\t' || c == '\r' || c == '\u00A0' || c == '\u2007' + || c == '\u202F' || c == '\uFEFF' || c == '>') { + continue; + } + return true; + } + + return false; + } + + private static String extractAttributes(String type, String element) { + + String ctype = ""; + if (ctypes.containsKey(type)) { + ctype = " ctype=\"" + ctypes.get(type) + "\""; + } + String result = ""; + element = cleanString(element); + + Vector v = translatableAttributes.get(type); + + StringTokenizer tokenizer = new StringTokenizer(element, "&= \t\n\r\f/", true); + while (tokenizer.hasMoreTokens()) { + String token = tokenizer.nextToken(); + if (!v.contains(token.toLowerCase())) { + result = result + token; + } else { + result = result + token; + String s = tokenizer.nextToken(); + while (s.equals("=") || s.equals(" ")) { + result = result + s; + s = tokenizer.nextToken(); + } + // s contains the first word of the atttribute + if ((s.startsWith("\"") && s.endsWith("\"") || s.startsWith("'") && s.endsWith("'")) + && s.length() > 1) { + // the value is one word and it is quoted + result = result + s.substring(0, 1) + "\u2029" + s.substring(1, s.length() - 1) + + "\u2029" + s.substring(s.length() - 1); + } else { + if (s.startsWith("\"") || s.startsWith("'")) { + // attribute value is quoted, but it has more than one word + String quote = s.substring(0, 1); + result = result + s.substring(0, 1) + "\u2029" + s.substring(1); + s = tokenizer.nextToken(); + do { + result = result + s; + s = tokenizer.nextToken(); + } while (s.indexOf(quote) == -1); + String left = s.substring(0, s.indexOf(quote)); + String right = s.substring(s.indexOf(quote)); + result = result + left + "\u2029" + right; + } else { + // attribute is not quoted, it can only be one word + result = result + "\u2029" + s + "\u2029"; + } + } + } + } + result = result + ""; + return result; + } + + private static void buildTables(String iniFile) throws SAXException, IOException, ParserConfigurationException { + + SAXBuilder builder = new SAXBuilder(); + Catalog cat = new Catalog(catalog); + builder.setEntityResolver(cat); + Document doc = builder.build(iniFile); + Element root = doc.getRootElement(); + List tags = root.getChildren("tag"); + + startsSegment = new Hashtable<>(); + translatableAttributes = new Hashtable<>(); + entities = new Hashtable<>(); + ctypes = new Hashtable<>(); + keepFormating = new Hashtable<>(); + + Iterator i = tags.iterator(); + while (i.hasNext()) { + Element t = i.next(); + if (t.getAttributeValue("hard-break", "inline").equals("segment")) { + startsSegment.put(t.getText(), "yes"); + } + if (t.getAttributeValue("keep-format", "no").equals("yes")) { + keepFormating.put(t.getText(), "yes"); + } + String attributes = t.getAttributeValue("attributes", ""); + if (!attributes.equals("")) { + StringTokenizer tokenizer = new StringTokenizer(attributes, ";"); + int count = tokenizer.countTokens(); + Vector v = new Vector<>(count); + for (int j = 0; j < count; j++) { + v.add(tokenizer.nextToken()); + } + translatableAttributes.put(t.getText(), v); + } + String ctype = t.getAttributeValue("ctype", ""); + if (!ctype.equals("")) { + ctypes.put(t.getText(), ctype); + } + } + + List ents = root.getChildren("entity"); + Iterator it = ents.iterator(); + while (it.hasNext()) { + Element e = it.next(); + entities.put("&" + e.getAttributeValue("name") + ";", e.getText()); + } + } + + private static void buildList(String file) throws IOException { + segments = new ArrayList<>(); + int start = file.indexOf('<'); + int end = file.indexOf('>'); + String type; + String text = ""; + + if (start > 0) { + segments.add(file.substring(0, start)); + file = file.substring(start); + start = file.indexOf('<'); + end = file.indexOf('>'); + } + while (start != -1) { + if (file.substring(start, start + 3).equals("") + 2; + } + if (end < start || end < 0 || start < 0) { + throw new IOException("nvalid HTML markup found."); + } + String element = file.substring(start, end + 1); + + // check if the element is OK or has strange stuff + // from Adobe GoLive 5 + int errors = element.indexOf('<', 1); + if (errors != -1 && !element.startsWith("') { + exit = true; + } + } + element = buffer.toString(); + end = start + buffer.toString().length(); + start = end; + } + + type = getType(element); + + if (type.equals("![INCLUDE[") || type.equals("![IGNORE[") || type.equals("![CDATA[")) { + // it's an SGML section, send it to skeleton + // and keep looking for segments + segments.add(text); + text = ""; + end = file.indexOf("]]>"); + segments.add(file.substring(start, end + 3)); + file = file.substring(end + 3); + start = file.indexOf('<'); + end = file.indexOf('>'); + if (start != -1) { + text = file.substring(0, start); + } + continue; + } + if (startsSegment.containsKey(type)) { + segments.add(text); + file = file.substring(text.length()); + start = start - text.length(); + end = end - text.length(); + text = ""; + } + if (type.startsWith("/") && startsSegment.containsKey(type.substring(1))) { + segments.add(text); + file = file.substring(text.length()); + start = start - text.length(); + end = end - text.length(); + text = ""; + } + if (type.equals("script") || type.equals("style")) { + // send the whole element to the list + segments.add(text); + file = file.substring(text.length()); + text = ""; + if (!element.endsWith("/>")) { + end = file.toLowerCase().indexOf("/" + type + ">"); + String script = file.substring(0, end + 2 + type.length()); + if (script.indexOf("") == -1) { + // there are nested scripts inside this one + end = file.toLowerCase().indexOf("/" + type + ">", file.indexOf("-->") + 3); + script = file.substring(0, end + 2 + type.length()); + } + segments.add(script); + file = file.substring(end + 2 + type.length()); + } else { + segments.add(element); + file = file.substring(element.length()); + } + + start = file.indexOf('<'); + end = file.indexOf('>'); + if (start != -1) { + text = file.substring(0, start); + } + continue; + } + if (type.equals("!--")) { + // it's a comment, send it to skeleton + // and keep looking for segments + segments.add(text); + file = file.substring(text.length()); + text = ""; + end = file.indexOf("-->"); + String comment = file.substring(0, end + 3); + segments.add(comment); + file = file.substring(end + 3); + start = file.indexOf('<'); + end = file.indexOf('>'); + if (start != -1) { + text = file.substring(0, start); + } + continue; + } + + text = text + element; + + if (end < file.length()) { // there may be text to extract + int nextTag = file.indexOf('<', end); + if (nextTag != -1) { + text += file.substring(end + 1, nextTag); + } + } + + if (start < file.length()) { + start++; + } + if (end < file.length()) { + end = file.indexOf('>', end + 1); + } else { + end = file.length(); + } + start = file.indexOf('<', start); + } + segments.add(text); + } + + private static String getType(String string) { + + String result = ""; + if (string.startsWith("') { + break; + } + result = result + c; + } + if (result.endsWith("/") && result.length() > 1) { + result = result.substring(0, result.length() - 1); + } + return result.toLowerCase(); + } + + private static boolean containsText(String string) { + StringBuilder buffer = new StringBuilder(); + int length = string.length(); + boolean inTag = false; + + for (int i = 0; i < length; i++) { + char c = string.charAt(i); + if (string.substring(i).startsWith("")) { + inTag = false; + i = i + 4; + continue; + } + if (!inTag) { + buffer.append(c); + } + } + return !buffer.toString().trim().equals(""); + } + +} diff --git a/src/com/maxprograms/converters/html/Xliff2Html.java b/src/com/maxprograms/converters/html/Xliff2Html.java index cf1d3640..78315f50 100644 --- a/src/com/maxprograms/converters/html/Xliff2Html.java +++ b/src/com/maxprograms/converters/html/Xliff2Html.java @@ -1,260 +1,260 @@ -/******************************************************************************* - * Copyright (c) 2003-2019 Maxprograms. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 1.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/org/documents/epl-v10.html - * - * Contributors: - * Maxprograms - initial API and implementation - *******************************************************************************/ -package com.maxprograms.converters.html; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.List; -import java.util.Vector; -import java.lang.System.Logger.Level; -import java.lang.System.Logger; - -import javax.xml.parsers.ParserConfigurationException; - -import org.xml.sax.SAXException; - -import com.maxprograms.xml.Catalog; -import com.maxprograms.xml.Document; -import com.maxprograms.xml.Element; -import com.maxprograms.xml.SAXBuilder; -import com.maxprograms.xml.TextNode; -import com.maxprograms.xml.XMLNode; - -public class Xliff2Html { - - private static String xliffFile; - private static Hashtable segments; - private static FileOutputStream output; - private static String encoding; - private static Hashtable entities; - private static Catalog catalog; - - private Xliff2Html() { - // do not instantiate this class - // use run method instead - } - - public static Vector run(Hashtable params) { - - Vector result = new Vector<>(); - - String sklFile = params.get("skeleton"); - xliffFile = params.get("xliff"); - encoding = params.get("encoding"); - - try { - catalog = new Catalog(params.get("catalog")); - loadEntities(); - String outputFile = params.get("backfile"); - File f = new File(outputFile); - File p = f.getParentFile(); - if (p == null) { - p = new File(System.getProperty("user.dir")); - } - if (!p.exists()) { - p.mkdirs(); - } - if (!f.exists()) { - Files.createFile(Paths.get(f.toURI())); - } - output = new FileOutputStream(f); - loadSegments(); - - try (InputStreamReader input = new InputStreamReader(new FileInputStream(sklFile), - StandardCharsets.UTF_8)) { - BufferedReader buffer = new BufferedReader(input); - String line; - while ((line = buffer.readLine()) != null) { - line = line + "\n"; - - if (line.indexOf("%%%") != -1) { - // - // contains translatable text - // - int index = line.indexOf("%%%"); - while (index != -1) { - String start = line.substring(0, index); - writeString(start); - line = line.substring(index + 3); - String code = line.substring(0, line.indexOf("%%%")); - line = line.substring(line.indexOf("%%%\n") + 4); - Element segment = segments.get(code); - if (segment != null) { - Element target = segment.getChild("target"); - Element source = segment.getChild("source"); - if (target != null) { - if (segment.getAttributeValue("approved", "no").equals("yes")) { - writeString(extractText(target)); - } else { - writeString(extractText(source)); - } - } else { - writeString(extractText(source)); - } - } else { - result.add(0, "1"); - result.add(1, "segment " + code + " not found"); - return result; - } - - index = line.indexOf("%%%"); - if (index == -1) { - writeString(line); - } - } // end while - } else { - // - // non translatable portion - // - writeString(line); - } - } - - output.close(); - } - result.add("0"); - - } catch (IOException | SAXException | ParserConfigurationException e) { - Logger logger = System.getLogger(Xliff2Html.class.getName()); - logger.log(Level.ERROR, "Error merging HTML file", e); - result.add("1"); - result.add(e.getMessage()); - return result; - } - return result; - } - - private static String extractText(Element target) { - String result = ""; - List content = target.getContent(); - Iterator i = content.iterator(); - while (i.hasNext()) { - XMLNode n = i.next(); - if (n.getNodeType() == XMLNode.ELEMENT_NODE) { - Element e = (Element) n; - result = result + extractText(e); - } - if (n.getNodeType() == XMLNode.TEXT_NODE) { - result = result + ((TextNode) n).getText(); - } - } - return addEntities(result); - } - - private static void loadEntities() throws SAXException, IOException, ParserConfigurationException { - SAXBuilder builder = new SAXBuilder(); - Document doc = builder.build(Xliff2Html.class.getResource("entities.xml")); - Element root = doc.getRootElement(); - - entities = new Hashtable<>(); - - List ents = root.getChildren("entity"); - Iterator it = ents.iterator(); - while (it.hasNext()) { - Element e = it.next(); - entities.put(e.getText(), "&" + e.getAttributeValue("name") + ";"); - } - } - - private static String addEntities(String text) { - StringBuilder result = new StringBuilder(); - boolean inTag = false; - int start = text.indexOf('<'); - int end = text.indexOf('>'); - if (end != -1) { - if (start == -1) { - inTag = true; - } else { - if (end < start) { - inTag = true; - } - } - } - for (int i = 0; i < text.length(); i++) { - char c = text.charAt(i); - if (c == '<') { - inTag = true; - } - if (c == '>') { - inTag = false; - } - if (!inTag && entities.containsKey("" + c)) { - if (c == '&' && text.charAt(i + 1) == '#') { - // check if it is an escaped entity - // like: &#x2018; - int scolon = text.indexOf(';', i); - if (scolon == -1) { - // not an escaped entity - result.append(entities.get("" + c)); - } else { - // check for space before the semicolon - int space = text.indexOf(' ', i); - if (space == -1) { - // no space before the semicolon - // it is an escaped entity - result.append(c); - } else { - if (space > scolon) { - // space is after semicolon - // it is an escaped entity - result.append(c); - } else { - // not an escaped entity - result.append(entities.get("" + c)); - } - } - } - } else { - result.append(entities.get("" + c)); - } - } else { - result.append(c); - } - } - return result.toString(); - } - - private static void writeString(String string) throws IOException { - output.write(string.getBytes(encoding)); - } - - private static void loadSegments() throws SAXException, IOException, ParserConfigurationException { - - SAXBuilder builder = new SAXBuilder(); - if (catalog != null) { - builder.setEntityResolver(catalog); - } - - Document doc = builder.build(xliffFile); - Element root = doc.getRootElement(); - Element body = root.getChild("file").getChild("body"); - List units = body.getChildren("trans-unit"); - Iterator i = units.iterator(); - - segments = new Hashtable<>(); - - while (i.hasNext()) { - Element unit = i.next(); - segments.put(unit.getAttributeValue("id"), unit); - } - } - -} +/******************************************************************************* + * Copyright (c) 2003-2019 Maxprograms. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/org/documents/epl-v10.html + * + * Contributors: + * Maxprograms - initial API and implementation + *******************************************************************************/ +package com.maxprograms.converters.html; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Vector; +import java.lang.System.Logger.Level; +import java.lang.System.Logger; + +import javax.xml.parsers.ParserConfigurationException; + +import org.xml.sax.SAXException; + +import com.maxprograms.converters.Constants; +import com.maxprograms.xml.Catalog; +import com.maxprograms.xml.Document; +import com.maxprograms.xml.Element; +import com.maxprograms.xml.SAXBuilder; +import com.maxprograms.xml.TextNode; +import com.maxprograms.xml.XMLNode; + +public class Xliff2Html { + + private static String xliffFile; + private static Hashtable segments; + private static FileOutputStream output; + private static String encoding; + private static Hashtable entities; + private static Catalog catalog; + + private Xliff2Html() { + // do not instantiate this class + // use run method instead + } + + public static Vector run(Hashtable params) { + + Vector result = new Vector<>(); + + String sklFile = params.get("skeleton"); + xliffFile = params.get("xliff"); + encoding = params.get("encoding"); + + try { + catalog = new Catalog(params.get("catalog")); + loadEntities(); + String outputFile = params.get("backfile"); + File f = new File(outputFile); + File p = f.getParentFile(); + if (p == null) { + p = new File(System.getProperty("user.dir")); + } + if (!p.exists()) { + p.mkdirs(); + } + if (!f.exists()) { + Files.createFile(Paths.get(f.toURI())); + } + output = new FileOutputStream(f); + loadSegments(); + + try (InputStreamReader input = new InputStreamReader(new FileInputStream(sklFile), + StandardCharsets.UTF_8)) { + BufferedReader buffer = new BufferedReader(input); + String line; + while ((line = buffer.readLine()) != null) { + line = line + "\n"; + + if (line.indexOf("%%%") != -1) { + // + // contains translatable text + // + int index = line.indexOf("%%%"); + while (index != -1) { + String start = line.substring(0, index); + writeString(start); + line = line.substring(index + 3); + String code = line.substring(0, line.indexOf("%%%")); + line = line.substring(line.indexOf("%%%\n") + 4); + Element segment = segments.get(code); + if (segment != null) { + Element target = segment.getChild("target"); + Element source = segment.getChild("source"); + if (target != null) { + if (segment.getAttributeValue("approved", "no").equals("yes")) { + writeString(extractText(target)); + } else { + writeString(extractText(source)); + } + } else { + writeString(extractText(source)); + } + } else { + result.add(Constants.ERROR); + result.add("segment " + code + " not found"); + return result; + } + + index = line.indexOf("%%%"); + if (index == -1) { + writeString(line); + } + } // end while + } else { + // + // non translatable portion + // + writeString(line); + } + } + + output.close(); + } + result.add(Constants.SUCCESS); + + } catch (IOException | SAXException | ParserConfigurationException e) { + Logger logger = System.getLogger(Xliff2Html.class.getName()); + logger.log(Level.ERROR, "Error merging HTML file", e); + result.add(Constants.ERROR); + result.add(e.getMessage()); + } + return result; + } + + private static String extractText(Element target) { + String result = ""; + List content = target.getContent(); + Iterator i = content.iterator(); + while (i.hasNext()) { + XMLNode n = i.next(); + if (n.getNodeType() == XMLNode.ELEMENT_NODE) { + Element e = (Element) n; + result = result + extractText(e); + } + if (n.getNodeType() == XMLNode.TEXT_NODE) { + result = result + ((TextNode) n).getText(); + } + } + return addEntities(result); + } + + private static void loadEntities() throws SAXException, IOException, ParserConfigurationException { + SAXBuilder builder = new SAXBuilder(); + Document doc = builder.build(Xliff2Html.class.getResource("entities.xml")); + Element root = doc.getRootElement(); + + entities = new Hashtable<>(); + + List ents = root.getChildren("entity"); + Iterator it = ents.iterator(); + while (it.hasNext()) { + Element e = it.next(); + entities.put(e.getText(), "&" + e.getAttributeValue("name") + ";"); + } + } + + private static String addEntities(String text) { + StringBuilder result = new StringBuilder(); + boolean inTag = false; + int start = text.indexOf('<'); + int end = text.indexOf('>'); + if (end != -1) { + if (start == -1) { + inTag = true; + } else { + if (end < start) { + inTag = true; + } + } + } + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (c == '<') { + inTag = true; + } + if (c == '>') { + inTag = false; + } + if (!inTag && entities.containsKey("" + c)) { + if (c == '&' && text.charAt(i + 1) == '#') { + // check if it is an escaped entity + // like: &#x2018; + int scolon = text.indexOf(';', i); + if (scolon == -1) { + // not an escaped entity + result.append(entities.get("" + c)); + } else { + // check for space before the semicolon + int space = text.indexOf(' ', i); + if (space == -1) { + // no space before the semicolon + // it is an escaped entity + result.append(c); + } else { + if (space > scolon) { + // space is after semicolon + // it is an escaped entity + result.append(c); + } else { + // not an escaped entity + result.append(entities.get("" + c)); + } + } + } + } else { + result.append(entities.get("" + c)); + } + } else { + result.append(c); + } + } + return result.toString(); + } + + private static void writeString(String string) throws IOException { + output.write(string.getBytes(encoding)); + } + + private static void loadSegments() throws SAXException, IOException, ParserConfigurationException { + + SAXBuilder builder = new SAXBuilder(); + if (catalog != null) { + builder.setEntityResolver(catalog); + } + + Document doc = builder.build(xliffFile); + Element root = doc.getRootElement(); + Element body = root.getChild("file").getChild("body"); + List units = body.getChildren("trans-unit"); + Iterator i = units.iterator(); + + segments = new Hashtable<>(); + + while (i.hasNext()) { + Element unit = i.next(); + segments.put(unit.getAttributeValue("id"), unit); + } + } + +} diff --git a/src/com/maxprograms/converters/idml/Idml2Xliff.java b/src/com/maxprograms/converters/idml/Idml2Xliff.java index 0861e9f0..5fae49a1 100644 --- a/src/com/maxprograms/converters/idml/Idml2Xliff.java +++ b/src/com/maxprograms/converters/idml/Idml2Xliff.java @@ -1,287 +1,288 @@ -/******************************************************************************* - * Copyright (c) 2003-2019 Maxprograms. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 1.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/org/documents/epl-v10.html - * - * Contributors: - * Maxprograms - initial API and implementation - *******************************************************************************/ -package com.maxprograms.converters.idml; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.List; -import java.util.Vector; -import java.lang.System.Logger.Level; -import java.lang.System.Logger; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; -import java.util.zip.ZipOutputStream; - -import javax.xml.parsers.ParserConfigurationException; - -import org.xml.sax.SAXException; - -import com.maxprograms.converters.Utils; -import com.maxprograms.xml.Document; -import com.maxprograms.xml.Element; -import com.maxprograms.xml.Indenter; -import com.maxprograms.xml.PI; -import com.maxprograms.xml.SAXBuilder; -import com.maxprograms.xml.XMLNode; -import com.maxprograms.xml.XMLOutputter; - -public class Idml2Xliff { - - private static final Logger LOGGER = System.getLogger(Idml2Xliff.class.getName()); - - private static Element mergedRoot; - private static String inputFile; - private static String skeleton; - private static ZipOutputStream out; - private static Vector used = null; - - private Idml2Xliff() { - // do not instantiate this class - // use run method instead - } - - private static void sortStories() { - List files = mergedRoot.getChildren("file"); - List instructions = mergedRoot.getPI(); - Hashtable table = new Hashtable<>(); - for (int i = 0; i < files.size(); i++) { - String key = getKey(files.get(i)); - table.put(key, i); - } - Vector v = new Vector<>(); - Iterator it = used.iterator(); - while (it.hasNext()) { - String key = it.next(); - if (table.containsKey(key)) { - v.add(files.get(table.get(key).intValue())); - } - } - mergedRoot.setContent(v); - Iterator pit = instructions.iterator(); - while (pit.hasNext()) { - mergedRoot.addContent(pit.next()); - } - } - - private static String getKey(Element file) { - List groups = file.getChild("header").getChildren("prop-group"); - for (int i = 0; i < groups.size(); i++) { - Element group = groups.get(i); - if (group.getAttributeValue("name").equals("document")) { - return group.getChild("prop").getText(); - } - } - return null; - } - - private static Vector getStories(String map) - throws SAXException, IOException, ParserConfigurationException { - Vector result = new Vector<>(); - SAXBuilder builder = new SAXBuilder(); - Document doc = builder.build(map); - Element root = doc.getRootElement(); - List stories = root.getChildren("idPkg:Story"); - Iterator it = stories.iterator(); - while (it.hasNext()) { - result.add(it.next().getAttributeValue("src")); - } - return result; - } - - private static void addFile(String xliff) throws SAXException, IOException, ParserConfigurationException { - SAXBuilder builder = new SAXBuilder(); - Document doc = builder.build(xliff); - Element root = doc.getRootElement(); - Element file = root.getChild("file"); - Element newFile = new Element("file"); - newFile.clone(file); - mergedRoot.addContent(newFile); - } - - private static void updateXliff(String xliff, String original) - throws SAXException, IOException, ParserConfigurationException { - SAXBuilder builder = new SAXBuilder(); - Document doc = builder.build(xliff); - Element root = doc.getRootElement(); - Element file = root.getChild("file"); - file.setAttribute("datatype", "x-idml"); - file.setAttribute("original", Utils.cleanString(inputFile)); - Element header = file.getChild("header"); - Element propGroup = new Element("prop-group"); - propGroup.setAttribute("name", "document"); - Element prop = new Element("prop"); - prop.setAttribute("prop-type", "original"); - prop.setText(original); - propGroup.addContent(prop); - header.addContent(propGroup); - - Element ext = header.getChild("skl").getChild("external-file"); - ext.setAttribute("href", Utils.cleanString(skeleton)); - - XMLOutputter outputter = new XMLOutputter(); - Indenter.indent(root, 2); - try (FileOutputStream output = new FileOutputStream(xliff)) { - outputter.output(doc, output); - } - } - - private static int countSegments(String string) throws SAXException, IOException, ParserConfigurationException { - SAXBuilder builder = new SAXBuilder(); - Document doc = builder.build(string); - Element root = doc.getRootElement(); - return root.getChild("file").getChild("body").getChildren("trans-unit").size(); - } - - private static void saveEntry(ZipEntry entry, String name) throws IOException { - ZipEntry content = new ZipEntry(entry.getName()); - content.setMethod(ZipEntry.DEFLATED); - out.putNextEntry(content); - try (FileInputStream input = new FileInputStream(name)) { - byte[] array = new byte[1024]; - int len; - while ((len = input.read(array)) > 0) { - out.write(array, 0, len); - } - out.closeEntry(); - } - } - - public static Vector run(Hashtable params) { - Vector result = new Vector<>(); - - inputFile = params.get("source"); - String xliff = params.get("xliff"); - skeleton = params.get("skeleton"); - String encoding = params.get("srcEncoding"); - - try { - Document merged = new Document(null, "xliff", null, null); - mergedRoot = merged.getRootElement(); - mergedRoot.setAttribute("version", "1.2"); - mergedRoot.setAttribute("xmlns", "urn:oasis:names:tc:xliff:document:1.2"); - mergedRoot.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); - mergedRoot.setAttribute("xsi:schemaLocation", - "urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd"); - mergedRoot.addContent(new PI("encoding", encoding)); - - out = new ZipOutputStream(new FileOutputStream(skeleton)); - try (ZipInputStream in = new ZipInputStream(new FileInputStream(inputFile))) { - - ZipEntry entry = null; - while ((entry = in.getNextEntry()) != null) { - if (entry.getName().matches(".*Story_.*\\.xml") - && (used != null && used.contains(entry.getName()))) { - File f = new File(entry.getName()); - String name = f.getName(); - File tmp = File.createTempFile(name.substring(0, name.lastIndexOf('.')), ".xml", - new File(skeleton).getParentFile()); - try (FileOutputStream output = new FileOutputStream(tmp.getAbsolutePath())) { - byte[] buf = new byte[1024]; - int len; - while ((len = in.read(buf)) > 0) { - output.write(buf, 0, len); - } - } - try { - Hashtable table = new Hashtable<>(); - table.put("source", tmp.getAbsolutePath()); - table.put("xliff", tmp.getAbsolutePath() + ".xlf"); - table.put("skeleton", tmp.getAbsolutePath() + ".skl"); - table.put("catalog", params.get("catalog")); - table.put("srcLang", params.get("srcLang")); - String tgtLang = params.get("tgtLang"); - if (tgtLang != null) { - table.put("tgtLang", tgtLang); - } - table.put("srcEncoding", params.get("srcEncoding")); - table.put("paragraph", params.get("paragraph")); - table.put("srxFile", params.get("srxFile")); - table.put("format", params.get("format")); - Vector res = null; - - res = Story2Xliff.run(table); - - if ("0".equals(res.get(0))) { - if (countSegments(tmp.getAbsolutePath() + ".xlf") > 0) { - updateXliff(tmp.getAbsolutePath() + ".xlf", entry.getName()); - addFile(tmp.getAbsolutePath() + ".xlf"); - ZipEntry content = new ZipEntry(entry.getName() + ".skl"); - content.setMethod(ZipEntry.DEFLATED); - out.putNextEntry(content); - try (FileInputStream input = new FileInputStream(tmp.getAbsolutePath() + ".skl")) { - byte[] array = new byte[1024]; - int len; - while ((len = input.read(array)) > 0) { - out.write(array, 0, len); - } - out.closeEntry(); - } - } else { - saveEntry(entry, tmp.getAbsolutePath()); - } - File skl = new File(tmp.getAbsolutePath() + ".skl"); - Files.delete(Paths.get(skl.toURI())); - File xlf = new File(tmp.getAbsolutePath() + ".xlf"); - Files.delete(Paths.get(xlf.toURI())); - } else { - saveEntry(entry, tmp.getAbsolutePath()); - } - } catch (Exception e) { - // do nothing - saveEntry(entry, tmp.getAbsolutePath()); - } - Files.delete(Paths.get(tmp.toURI())); - } else { - // not a story - File tmp = File.createTempFile("zip", ".tmp"); - try (FileOutputStream output = new FileOutputStream(tmp.getAbsolutePath())) { - byte[] buf = new byte[1024]; - int len; - while ((len = in.read(buf)) > 0) { - output.write(buf, 0, len); - } - } - if (entry.getName().matches(".*designmap\\.xml")) { - used = getStories(tmp.getAbsolutePath()); - } - saveEntry(entry, tmp.getAbsolutePath()); - Files.delete(Paths.get(tmp.toURI())); - } - } - - } - out.close(); - - sortStories(); - - // output final XLIFF - - XMLOutputter outputter = new XMLOutputter(); - outputter.preserveSpace(true); - try (FileOutputStream output = new FileOutputStream(xliff)) { - outputter.output(merged, output); - } - result.add("0"); - } catch (IOException | SAXException | ParserConfigurationException e) { - LOGGER.log(Level.ERROR, "Error converting IDML file", e); - result.add("1"); - result.add(e.getMessage()); - } - return result; - } -} +/******************************************************************************* + * Copyright (c) 2003-2019 Maxprograms. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/org/documents/epl-v10.html + * + * Contributors: + * Maxprograms - initial API and implementation + *******************************************************************************/ +package com.maxprograms.converters.idml; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Vector; +import java.lang.System.Logger.Level; +import java.lang.System.Logger; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +import javax.xml.parsers.ParserConfigurationException; + +import org.xml.sax.SAXException; + +import com.maxprograms.converters.Constants; +import com.maxprograms.converters.Utils; +import com.maxprograms.xml.Document; +import com.maxprograms.xml.Element; +import com.maxprograms.xml.Indenter; +import com.maxprograms.xml.PI; +import com.maxprograms.xml.SAXBuilder; +import com.maxprograms.xml.XMLNode; +import com.maxprograms.xml.XMLOutputter; + +public class Idml2Xliff { + + private static final Logger LOGGER = System.getLogger(Idml2Xliff.class.getName()); + + private static Element mergedRoot; + private static String inputFile; + private static String skeleton; + private static ZipOutputStream out; + private static Vector used = null; + + private Idml2Xliff() { + // do not instantiate this class + // use run method instead + } + + private static void sortStories() { + List files = mergedRoot.getChildren("file"); + List instructions = mergedRoot.getPI(); + Hashtable table = new Hashtable<>(); + for (int i = 0; i < files.size(); i++) { + String key = getKey(files.get(i)); + table.put(key, i); + } + Vector v = new Vector<>(); + Iterator it = used.iterator(); + while (it.hasNext()) { + String key = it.next(); + if (table.containsKey(key)) { + v.add(files.get(table.get(key).intValue())); + } + } + mergedRoot.setContent(v); + Iterator pit = instructions.iterator(); + while (pit.hasNext()) { + mergedRoot.addContent(pit.next()); + } + } + + private static String getKey(Element file) { + List groups = file.getChild("header").getChildren("prop-group"); + for (int i = 0; i < groups.size(); i++) { + Element group = groups.get(i); + if (group.getAttributeValue("name").equals("document")) { + return group.getChild("prop").getText(); + } + } + return null; + } + + private static Vector getStories(String map) + throws SAXException, IOException, ParserConfigurationException { + Vector result = new Vector<>(); + SAXBuilder builder = new SAXBuilder(); + Document doc = builder.build(map); + Element root = doc.getRootElement(); + List stories = root.getChildren("idPkg:Story"); + Iterator it = stories.iterator(); + while (it.hasNext()) { + result.add(it.next().getAttributeValue("src")); + } + return result; + } + + private static void addFile(String xliff) throws SAXException, IOException, ParserConfigurationException { + SAXBuilder builder = new SAXBuilder(); + Document doc = builder.build(xliff); + Element root = doc.getRootElement(); + Element file = root.getChild("file"); + Element newFile = new Element("file"); + newFile.clone(file); + mergedRoot.addContent(newFile); + } + + private static void updateXliff(String xliff, String original) + throws SAXException, IOException, ParserConfigurationException { + SAXBuilder builder = new SAXBuilder(); + Document doc = builder.build(xliff); + Element root = doc.getRootElement(); + Element file = root.getChild("file"); + file.setAttribute("datatype", "x-idml"); + file.setAttribute("original", Utils.cleanString(inputFile)); + Element header = file.getChild("header"); + Element propGroup = new Element("prop-group"); + propGroup.setAttribute("name", "document"); + Element prop = new Element("prop"); + prop.setAttribute("prop-type", "original"); + prop.setText(original); + propGroup.addContent(prop); + header.addContent(propGroup); + + Element ext = header.getChild("skl").getChild("external-file"); + ext.setAttribute("href", Utils.cleanString(skeleton)); + + XMLOutputter outputter = new XMLOutputter(); + Indenter.indent(root, 2); + try (FileOutputStream output = new FileOutputStream(xliff)) { + outputter.output(doc, output); + } + } + + private static int countSegments(String string) throws SAXException, IOException, ParserConfigurationException { + SAXBuilder builder = new SAXBuilder(); + Document doc = builder.build(string); + Element root = doc.getRootElement(); + return root.getChild("file").getChild("body").getChildren("trans-unit").size(); + } + + private static void saveEntry(ZipEntry entry, String name) throws IOException { + ZipEntry content = new ZipEntry(entry.getName()); + content.setMethod(ZipEntry.DEFLATED); + out.putNextEntry(content); + try (FileInputStream input = new FileInputStream(name)) { + byte[] array = new byte[1024]; + int len; + while ((len = input.read(array)) > 0) { + out.write(array, 0, len); + } + out.closeEntry(); + } + } + + public static Vector run(Hashtable params) { + Vector result = new Vector<>(); + + inputFile = params.get("source"); + String xliff = params.get("xliff"); + skeleton = params.get("skeleton"); + String encoding = params.get("srcEncoding"); + + try { + Document merged = new Document(null, "xliff", null, null); + mergedRoot = merged.getRootElement(); + mergedRoot.setAttribute("version", "1.2"); + mergedRoot.setAttribute("xmlns", "urn:oasis:names:tc:xliff:document:1.2"); + mergedRoot.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); + mergedRoot.setAttribute("xsi:schemaLocation", + "urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd"); + mergedRoot.addContent(new PI("encoding", encoding)); + + out = new ZipOutputStream(new FileOutputStream(skeleton)); + try (ZipInputStream in = new ZipInputStream(new FileInputStream(inputFile))) { + + ZipEntry entry = null; + while ((entry = in.getNextEntry()) != null) { + if (entry.getName().matches(".*Story_.*\\.xml") + && (used != null && used.contains(entry.getName()))) { + File f = new File(entry.getName()); + String name = f.getName(); + File tmp = File.createTempFile(name.substring(0, name.lastIndexOf('.')), ".xml", + new File(skeleton).getParentFile()); + try (FileOutputStream output = new FileOutputStream(tmp.getAbsolutePath())) { + byte[] buf = new byte[1024]; + int len; + while ((len = in.read(buf)) > 0) { + output.write(buf, 0, len); + } + } + try { + Hashtable table = new Hashtable<>(); + table.put("source", tmp.getAbsolutePath()); + table.put("xliff", tmp.getAbsolutePath() + ".xlf"); + table.put("skeleton", tmp.getAbsolutePath() + ".skl"); + table.put("catalog", params.get("catalog")); + table.put("srcLang", params.get("srcLang")); + String tgtLang = params.get("tgtLang"); + if (tgtLang != null) { + table.put("tgtLang", tgtLang); + } + table.put("srcEncoding", params.get("srcEncoding")); + table.put("paragraph", params.get("paragraph")); + table.put("srxFile", params.get("srxFile")); + table.put("format", params.get("format")); + Vector res = null; + + res = Story2Xliff.run(table); + + if (Constants.SUCCESS.equals(res.get(0))) { + if (countSegments(tmp.getAbsolutePath() + ".xlf") > 0) { + updateXliff(tmp.getAbsolutePath() + ".xlf", entry.getName()); + addFile(tmp.getAbsolutePath() + ".xlf"); + ZipEntry content = new ZipEntry(entry.getName() + ".skl"); + content.setMethod(ZipEntry.DEFLATED); + out.putNextEntry(content); + try (FileInputStream input = new FileInputStream(tmp.getAbsolutePath() + ".skl")) { + byte[] array = new byte[1024]; + int len; + while ((len = input.read(array)) > 0) { + out.write(array, 0, len); + } + out.closeEntry(); + } + } else { + saveEntry(entry, tmp.getAbsolutePath()); + } + File skl = new File(tmp.getAbsolutePath() + ".skl"); + Files.delete(Paths.get(skl.toURI())); + File xlf = new File(tmp.getAbsolutePath() + ".xlf"); + Files.delete(Paths.get(xlf.toURI())); + } else { + saveEntry(entry, tmp.getAbsolutePath()); + } + } catch (Exception e) { + // do nothing + saveEntry(entry, tmp.getAbsolutePath()); + } + Files.delete(Paths.get(tmp.toURI())); + } else { + // not a story + File tmp = File.createTempFile("zip", ".tmp"); + try (FileOutputStream output = new FileOutputStream(tmp.getAbsolutePath())) { + byte[] buf = new byte[1024]; + int len; + while ((len = in.read(buf)) > 0) { + output.write(buf, 0, len); + } + } + if (entry.getName().matches(".*designmap\\.xml")) { + used = getStories(tmp.getAbsolutePath()); + } + saveEntry(entry, tmp.getAbsolutePath()); + Files.delete(Paths.get(tmp.toURI())); + } + } + + } + out.close(); + + sortStories(); + + // output final XLIFF + + XMLOutputter outputter = new XMLOutputter(); + outputter.preserveSpace(true); + try (FileOutputStream output = new FileOutputStream(xliff)) { + outputter.output(merged, output); + } + result.add(Constants.SUCCESS); + } catch (IOException | SAXException | ParserConfigurationException e) { + LOGGER.log(Level.ERROR, "Error converting IDML file", e); + result.add(Constants.ERROR); + result.add(e.getMessage()); + } + return result; + } +} diff --git a/src/com/maxprograms/converters/idml/Story2Xliff.java b/src/com/maxprograms/converters/idml/Story2Xliff.java index 691abb50..f414f2f3 100644 --- a/src/com/maxprograms/converters/idml/Story2Xliff.java +++ b/src/com/maxprograms/converters/idml/Story2Xliff.java @@ -1,581 +1,582 @@ -/******************************************************************************* - * Copyright (c) 2003-2019 Maxprograms. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 1.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/org/documents/epl-v10.html - * - * Contributors: - * Maxprograms - initial API and implementation - *******************************************************************************/ -package com.maxprograms.converters.idml; - -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.List; -import java.util.Vector; -import java.lang.System.Logger.Level; -import java.lang.System.Logger; - -import javax.xml.parsers.ParserConfigurationException; - -import org.xml.sax.SAXException; - -import com.maxprograms.converters.Utils; -import com.maxprograms.segmenter.Segmenter; -import com.maxprograms.xml.Attribute; -import com.maxprograms.xml.Document; -import com.maxprograms.xml.Element; -import com.maxprograms.xml.PI; -import com.maxprograms.xml.SAXBuilder; -import com.maxprograms.xml.TextNode; -import com.maxprograms.xml.XMLNode; - -public class Story2Xliff { - private static String inputFile; - private static String skeletonFile; - private static String sourceLanguage; - private static String targetLanguage; - private static String srcEncoding; - private static Segmenter segmenter; - private static FileOutputStream output; - private static FileOutputStream skeleton; - private static int id = 1; - - private Story2Xliff() { - // do not instantiate this class - // use run method instead - } - - public static Vector run(Hashtable params) { - Vector result = new Vector<>(); - id = 1; - inputFile = params.get("source"); - String xliffFile = params.get("xliff"); - skeletonFile = params.get("skeleton"); - sourceLanguage = params.get("srcLang"); - targetLanguage = params.get("tgtLang"); - srcEncoding = params.get("srcEncoding"); - String elementSegmentation = params.get("paragraph"); - String initSegmenter = params.get("srxFile"); - String catalog = params.get("catalog"); - boolean paragraphSegmentation = false; - - if (elementSegmentation == null) { - paragraphSegmentation = false; - } else { - paragraphSegmentation = elementSegmentation.equals("yes"); - } - - try { - if (!paragraphSegmentation) { - segmenter = new Segmenter(initSegmenter, sourceLanguage, catalog); - } - skeleton = new FileOutputStream(skeletonFile); - output = new FileOutputStream(xliffFile); - writeHeader(); - - SAXBuilder builder = new SAXBuilder(); - Document doc = builder.build(inputFile); - Element root = doc.getRootElement(); - - removeChangeTracking(root); - - writeSkeleton("\n"); - List pis = doc.getPI(); - for (int i = 0; i < pis.size(); i++) { - writeSkeleton(pis.get(i).toString() + "\n"); - } - writeSkeleton("<" + root.getName()); - List atts = root.getAttributes(); - Iterator ia = atts.iterator(); - while (ia.hasNext()) { - Attribute a = ia.next(); - writeSkeleton( - " " + a.getName() + "=\"" + Utils.cleanString(a.getValue()).replaceAll("\"", ""e;") + "\""); //$NON-NLS-5$ - } - writeSkeleton(">"); - - List content = root.getContent(); - Iterator it = content.iterator(); - while (it.hasNext()) { - XMLNode n = it.next(); - if (n.getNodeType() == XMLNode.ELEMENT_NODE) { - Element e = (Element) n; - if (e.getName().equals("Story")) { - writeSkeleton("<" + e.getName()); - atts = e.getAttributes(); - ia = atts.iterator(); - while (ia.hasNext()) { - Attribute a = ia.next(); - writeSkeleton(" " + a.getName() + "=\"" - + Utils.cleanString(a.getValue()).replaceAll("\"", ""e;") + "\""); //$NON-NLS-3$ - } - writeSkeleton(">"); - processStory(e); - writeSkeleton(""); - } else { - writeSkeleton(e.toString()); - } - } - if (n.getNodeType() == XMLNode.TEXT_NODE) { - writeSkeleton(((TextNode) n).getText()); - } - if (n.getNodeType() == XMLNode.PROCESSING_INSTRUCTION_NODE) { - writeSkeleton(n.toString()); - } - if (n.getNodeType() == XMLNode.CDATA_SECTION_NODE) { - writeSkeleton(n.toString()); - } - } - - writeSkeleton(""); - skeleton.close(); - - writeString("\n"); - writeString("
\n"); - writeString("
"); - output.close(); - - result.add("0"); - } catch (IOException | SAXException | ParserConfigurationException e) { - Logger logger = System.getLogger(Story2Xliff.class.getName()); - logger.log(Level.ERROR, "Error converting Story", e); - result.add("1"); - result.add(e.getMessage()); - } - return result; - } - - private static void removeChangeTracking(Element root) { - Vector newContent = new Vector<>(); - List nodes = root.getContent(); - Iterator it = nodes.iterator(); - boolean changed = false; - while (it.hasNext()) { - XMLNode n = it.next(); - if (n.getNodeType() == XMLNode.ELEMENT_NODE) { - Element e = (Element) n; - if (e.getName().equals("Change")) { - if (e.getAttributeValue("ChangeType", "").equals("DeletedText")) { - changed = true; - } else { - newContent.addAll(e.getContent()); - changed = true; - } - } else { - newContent.add(n); - } - } else { - newContent.add(n); - } - } - if (changed) { - root.setContent(newContent); - mergeContent(root); - } - List children = root.getChildren(); - for (int i = 0; i < children.size(); i++) { - Element child = children.get(i); - removeChangeTracking(child); - } - } - - private static void mergeContent(Element e) { - Vector newContent = new Vector<>(); - Element current = null; - List content = e.getContent(); - Iterator it = content.iterator(); - while (it.hasNext()) { - XMLNode node = it.next(); - if (node.getNodeType() == XMLNode.ELEMENT_NODE) { - if (current == null) { - current = (Element) node; - newContent.add(node); - } else { - Element next = (Element) node; - if (current.getName().equals(next.getName()) && validateAttributes(current, next)) { - List nodes = next.getContent(); - Iterator nt = nodes.iterator(); - while (nt.hasNext()) { - current.addContent(nt.next()); - } - } else { - newContent.add(next); - current = next; - } - } - } else { - newContent.add(node); - } - } - e.setContent(newContent); - List children = e.getChildren(); - for (int i = 0; i < children.size(); i++) { - mergeContent(children.get(i)); - } - } - - private static boolean validateAttributes(Element current, Element next) { - List currentList = current.getAttributes(); - List nextList = next.getAttributes(); - if (currentList.size() != nextList.size()) { - return false; - } - for (int i = 0; i < currentList.size(); i++) { - Attribute a = currentList.get(i); - if (!a.getValue().equals(next.getAttributeValue(a.getName(), ""))) { - return false; - } - } - return true; - } - - private static void writeSkeleton(String string) throws IOException { - skeleton.write(string.getBytes(StandardCharsets.UTF_8)); - } - - private static void processStory(Element root) throws IOException { - List content = root.getContent(); - Iterator nit = content.iterator(); - while (nit.hasNext()) { - XMLNode n = nit.next(); - if (n.getNodeType() == XMLNode.TEXT_NODE) { - writeSkeleton(n.toString()); - } - if (n.getNodeType() == XMLNode.ELEMENT_NODE) { - Element e = (Element) n; - if (e.getName().equals("ParagraphStyleRange")) { - if (hasText(e)) { - processPara(e); - } else { - writeSkeleton(e.toString()); - } - } else { - if (e.getChildren().isEmpty()) { - writeSkeleton(e.toString()); - } else { - writeSkeleton("<" + e.getName()); - List atts = e.getAttributes(); - Iterator ia = atts.iterator(); - while (ia.hasNext()) { - Attribute a = ia.next(); - writeSkeleton(" " + a.getName() + "=\"" - + Utils.cleanString(a.getValue()).replaceAll("\"", ""e;") + "\""); //$NON-NLS-3$ - } - writeSkeleton(">"); - processStory(e); - writeSkeleton(""); - } - } - } - if (n.getNodeType() == XMLNode.PROCESSING_INSTRUCTION_NODE) { - writeSkeleton(n.toString()); - } - if (n.getNodeType() == XMLNode.CDATA_SECTION_NODE) { - writeSkeleton(n.toString()); - } - } - } - - private static void processPara(Element e) throws IOException { - cleanAttributes(e); - mergeStyles(e); - writeSkeleton("<" + e.getName()); - List atts = e.getAttributes(); - Iterator ia = atts.iterator(); - while (ia.hasNext()) { - Attribute a = ia.next(); - writeSkeleton( - " " + a.getName() + "=\"" + Utils.cleanString(a.getValue()).replaceAll("\"", ""e;") + "\""); //$NON-NLS-5$ - } - writeSkeleton(">"); - String source = ""; - List content = e.getContent(); - Iterator it = content.iterator(); - boolean preamble = true; - while (it.hasNext()) { - XMLNode n = it.next(); - if (n.getNodeType() == XMLNode.TEXT_NODE) { - source = source + n.toString(); - } - if (n.getNodeType() == XMLNode.CDATA_SECTION_NODE) { - source = source + n.toString(); - } - if (n.getNodeType() == XMLNode.ELEMENT_NODE) { - Element el = (Element) n; - if (hasText(el)) { - preamble = false; - source = source + recurseElement(el); - } else { - if (preamble) { - writeSkeleton(el.toString()); - } else { - source = source + el.toString(); - } - } - } - if (n.getNodeType() == XMLNode.PROCESSING_INSTRUCTION_NODE) { - source = source + n.toString(); - } - } - source = source + ""; - String[] parts = splitCell(source); - for (int i = 0; i < parts.length; i++) { - String s = parts[i]; - String first = s.substring(0, s.indexOf("") + 5); - writeSkeleton(first.substring("".length(), first.length() - "".length())); - if (!first.equals(s)) { - String center = s.substring(first.length(), s.lastIndexOf("")); - String last = s.substring(s.lastIndexOf("")); - String[] segments = null; - if (segmenter != null) { - segments = segmenter.segment(fixTags(center)); - } else { - segments = new String[] { fixTags(center) }; - } - for (int h = 0; h < segments.length; h++) { - if (segments[h].trim().equals("")) { - writeSkeleton(segments[h]); - } else { - writeSkeleton("%%%" + id + "%%%\n"); - writeString("\n" + segments[h] - + "\n\n"); - } - } - writeSkeleton(last.substring("".length(), last.length() - "".length())); - } - } - writeSkeleton(""); - } - - private static void mergeStyles(Element e) { - List newContent = new ArrayList<>(); - List content = e.getContent(); - Element current = null; - Element currentStyle = null; - for (int i = 0; i < content.size(); i++) { - XMLNode node = content.get(i); - if (node.getNodeType() == XMLNode.TEXT_NODE) { - newContent.add(node); - } - if (node.getNodeType() == XMLNode.ELEMENT_NODE) { - Element child = (Element) node; - if (child.getName().equals("CharacterStyleRange")) { - current = child; - currentStyle = getStyle(current); - int h = 1; - int skip = 0; - while (i + h < content.size()) { - XMLNode next = content.get(i + h); - if (next.getNodeType() == XMLNode.ELEMENT_NODE) { - Element n = (Element) next; - if (n.getName().equals("CharacterStyleRange")) { - Element nextStyle = getStyle(n); - if (currentStyle.equals(nextStyle)) { - String text = extractContent(n); - addContent(current, text); - skip = h; - } else { - break; - } - } else { - break; - } - } - h++; - } - newContent.add(node); - i = i + skip; - } else { - newContent.add(node); - } - } - } - e.setContent(newContent); - } - - private static void addContent(Element e, String text) { - Element content = e.getChild("Content"); - if (content != null) { - content.setText(content.getText() + text); - } - } - - private static String extractContent(Element e) { - Element content = e.getChild("Content"); - if (content != null) { - return content.getText(); - } - return ""; - } - - private static Element getStyle(Element e) { - Element result = new Element(e.getName()); - List atts = e.getAttributes(); - for (int i = 0; i < atts.size(); i++) { - Attribute a = atts.get(i); - result.setAttribute(a.getName(), a.getValue()); - } - List children = e.getChildren(); - Iterator it = children.iterator(); - while (it.hasNext()) { - Element child = it.next(); - if (!child.getName().equals("Content")) { - result.addContent(getStyle(child)); - } - } - return result; - } - - private static void cleanAttributes(Element e) { - Attribute a = e.getAttribute("CharacterDirection"); - if (a != null && a.getValue().equals("DefaultDirection")) { - e.removeAttribute("CharacterDirection"); - } - a = e.getAttribute("DiacriticPosition"); - if (a != null && a.getValue().equals("DefaultPosition")) { - e.removeAttribute("DiacriticPosition"); - } - a = e.getAttribute("DigitsType"); - if (a != null && a.getValue().equals("DefaultDigits")) { - e.removeAttribute("DigitsType"); - } - List children = e.getChildren(); - Iterator it = children.iterator(); - while (it.hasNext()) { - cleanAttributes(it.next()); - } - } - - private static String[] splitCell(String string) { - int index = string.indexOf(" v = new Vector<>(); - while (index != -1) { - v.add(string.substring(0, index) + ""); - string = "" + string.substring(index); - index = string.indexOf(" v2 = new Vector<>(); - for (int i = 0; i < v.size(); i++) { - String s = v.get(i); - int index2 = s.indexOf(""; - v2.add(str); - s = "" + s.substring(index2); - index2 = s.indexOf(""); - while (start != -1) { - result = result + string.substring(0, start); - string = string.substring(start); - int end = string.indexOf(""); - String clean = Utils.cleanString(string.substring("".length(), end)); - result = result + "" + clean + ""; - if (clean.indexOf('\u2028') != -1) { - result = result + "\n"; - } - string = string.substring(end + "".length()); - start = string.indexOf(""); - } - result = result + string; - return result; - } - - private static String recurseElement(Element e) { - String result = ""; - if (e.getName().equals("Content")) { - result = result + "" + Utils.cleanString(e.getText()) + ""; - } else { - result = result + "<" + e.getName(); - List atts = e.getAttributes(); - Iterator ia = atts.iterator(); - while (ia.hasNext()) { - Attribute a = ia.next(); - result = result + " " + a.getName() + "=\"" - + Utils.cleanString(a.getValue()).replaceAll("\"", ""e;") + "\""; //$NON-NLS-3$ - } - if (e.getContent().isEmpty()) { - result = result + " />"; - } else { - result = result + ">"; - List content = e.getContent(); - Iterator it = content.iterator(); - while (it.hasNext()) { - XMLNode n = it.next(); - if (n.getNodeType() == XMLNode.TEXT_NODE) { - result = result + n.toString(); - } - if (n.getNodeType() == XMLNode.ELEMENT_NODE) { - result = result + recurseElement((Element) n); - } - } - result = result + ""; - } - } - return result; - } - - private static boolean hasText(Element e) { - if (e.getName().equals("Content") && !e.getText().isEmpty()) { - return true; - } - List children = e.getChildren(); - Iterator it = children.iterator(); - while (it.hasNext()) { - if (hasText(it.next())) { - return true; - } - } - return false; - } - - private static void writeHeader() throws IOException { - String tgtLang = ""; - if (targetLanguage != null) { - tgtLang = "\" target-language=\"" + targetLanguage; - } - String format = "x-idml"; - writeString("\n"); - writeString("\n"); - writeString("\n"); - - writeString("\n"); - writeString("
\n"); - writeString(" \n"); - writeString(" \n"); - writeString(" \n"); - writeString("
\n"); - writeString("\n"); - } - - private static void writeString(String string) throws IOException { - output.write(string.getBytes(StandardCharsets.UTF_8)); - } -} +/******************************************************************************* + * Copyright (c) 2003-2019 Maxprograms. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/org/documents/epl-v10.html + * + * Contributors: + * Maxprograms - initial API and implementation + *******************************************************************************/ +package com.maxprograms.converters.idml; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Vector; +import java.lang.System.Logger.Level; +import java.lang.System.Logger; + +import javax.xml.parsers.ParserConfigurationException; + +import org.xml.sax.SAXException; + +import com.maxprograms.converters.Constants; +import com.maxprograms.converters.Utils; +import com.maxprograms.segmenter.Segmenter; +import com.maxprograms.xml.Attribute; +import com.maxprograms.xml.Document; +import com.maxprograms.xml.Element; +import com.maxprograms.xml.PI; +import com.maxprograms.xml.SAXBuilder; +import com.maxprograms.xml.TextNode; +import com.maxprograms.xml.XMLNode; + +public class Story2Xliff { + private static String inputFile; + private static String skeletonFile; + private static String sourceLanguage; + private static String targetLanguage; + private static String srcEncoding; + private static Segmenter segmenter; + private static FileOutputStream output; + private static FileOutputStream skeleton; + private static int id = 1; + + private Story2Xliff() { + // do not instantiate this class + // use run method instead + } + + public static Vector run(Hashtable params) { + Vector result = new Vector<>(); + id = 1; + inputFile = params.get("source"); + String xliffFile = params.get("xliff"); + skeletonFile = params.get("skeleton"); + sourceLanguage = params.get("srcLang"); + targetLanguage = params.get("tgtLang"); + srcEncoding = params.get("srcEncoding"); + String elementSegmentation = params.get("paragraph"); + String initSegmenter = params.get("srxFile"); + String catalog = params.get("catalog"); + boolean paragraphSegmentation = false; + + if (elementSegmentation == null) { + paragraphSegmentation = false; + } else { + paragraphSegmentation = elementSegmentation.equals("yes"); + } + + try { + if (!paragraphSegmentation) { + segmenter = new Segmenter(initSegmenter, sourceLanguage, catalog); + } + skeleton = new FileOutputStream(skeletonFile); + output = new FileOutputStream(xliffFile); + writeHeader(); + + SAXBuilder builder = new SAXBuilder(); + Document doc = builder.build(inputFile); + Element root = doc.getRootElement(); + + removeChangeTracking(root); + + writeSkeleton("\n"); + List pis = doc.getPI(); + for (int i = 0; i < pis.size(); i++) { + writeSkeleton(pis.get(i).toString() + "\n"); + } + writeSkeleton("<" + root.getName()); + List atts = root.getAttributes(); + Iterator ia = atts.iterator(); + while (ia.hasNext()) { + Attribute a = ia.next(); + writeSkeleton( + " " + a.getName() + "=\"" + Utils.cleanString(a.getValue()).replaceAll("\"", ""e;") + "\""); //$NON-NLS-5$ + } + writeSkeleton(">"); + + List content = root.getContent(); + Iterator it = content.iterator(); + while (it.hasNext()) { + XMLNode n = it.next(); + if (n.getNodeType() == XMLNode.ELEMENT_NODE) { + Element e = (Element) n; + if (e.getName().equals("Story")) { + writeSkeleton("<" + e.getName()); + atts = e.getAttributes(); + ia = atts.iterator(); + while (ia.hasNext()) { + Attribute a = ia.next(); + writeSkeleton(" " + a.getName() + "=\"" + + Utils.cleanString(a.getValue()).replaceAll("\"", ""e;") + "\""); //$NON-NLS-3$ + } + writeSkeleton(">"); + processStory(e); + writeSkeleton(""); + } else { + writeSkeleton(e.toString()); + } + } + if (n.getNodeType() == XMLNode.TEXT_NODE) { + writeSkeleton(((TextNode) n).getText()); + } + if (n.getNodeType() == XMLNode.PROCESSING_INSTRUCTION_NODE) { + writeSkeleton(n.toString()); + } + if (n.getNodeType() == XMLNode.CDATA_SECTION_NODE) { + writeSkeleton(n.toString()); + } + } + + writeSkeleton(""); + skeleton.close(); + + writeString("\n"); + writeString("
\n"); + writeString("
"); + output.close(); + + result.add(Constants.SUCCESS); + } catch (IOException | SAXException | ParserConfigurationException e) { + Logger logger = System.getLogger(Story2Xliff.class.getName()); + logger.log(Level.ERROR, "Error converting Story", e); + result.add(Constants.ERROR); + result.add(e.getMessage()); + } + return result; + } + + private static void removeChangeTracking(Element root) { + Vector newContent = new Vector<>(); + List nodes = root.getContent(); + Iterator it = nodes.iterator(); + boolean changed = false; + while (it.hasNext()) { + XMLNode n = it.next(); + if (n.getNodeType() == XMLNode.ELEMENT_NODE) { + Element e = (Element) n; + if (e.getName().equals("Change")) { + if (e.getAttributeValue("ChangeType", "").equals("DeletedText")) { + changed = true; + } else { + newContent.addAll(e.getContent()); + changed = true; + } + } else { + newContent.add(n); + } + } else { + newContent.add(n); + } + } + if (changed) { + root.setContent(newContent); + mergeContent(root); + } + List children = root.getChildren(); + for (int i = 0; i < children.size(); i++) { + Element child = children.get(i); + removeChangeTracking(child); + } + } + + private static void mergeContent(Element e) { + Vector newContent = new Vector<>(); + Element current = null; + List content = e.getContent(); + Iterator it = content.iterator(); + while (it.hasNext()) { + XMLNode node = it.next(); + if (node.getNodeType() == XMLNode.ELEMENT_NODE) { + if (current == null) { + current = (Element) node; + newContent.add(node); + } else { + Element next = (Element) node; + if (current.getName().equals(next.getName()) && validateAttributes(current, next)) { + List nodes = next.getContent(); + Iterator nt = nodes.iterator(); + while (nt.hasNext()) { + current.addContent(nt.next()); + } + } else { + newContent.add(next); + current = next; + } + } + } else { + newContent.add(node); + } + } + e.setContent(newContent); + List children = e.getChildren(); + for (int i = 0; i < children.size(); i++) { + mergeContent(children.get(i)); + } + } + + private static boolean validateAttributes(Element current, Element next) { + List currentList = current.getAttributes(); + List nextList = next.getAttributes(); + if (currentList.size() != nextList.size()) { + return false; + } + for (int i = 0; i < currentList.size(); i++) { + Attribute a = currentList.get(i); + if (!a.getValue().equals(next.getAttributeValue(a.getName(), ""))) { + return false; + } + } + return true; + } + + private static void writeSkeleton(String string) throws IOException { + skeleton.write(string.getBytes(StandardCharsets.UTF_8)); + } + + private static void processStory(Element root) throws IOException { + List content = root.getContent(); + Iterator nit = content.iterator(); + while (nit.hasNext()) { + XMLNode n = nit.next(); + if (n.getNodeType() == XMLNode.TEXT_NODE) { + writeSkeleton(n.toString()); + } + if (n.getNodeType() == XMLNode.ELEMENT_NODE) { + Element e = (Element) n; + if (e.getName().equals("ParagraphStyleRange")) { + if (hasText(e)) { + processPara(e); + } else { + writeSkeleton(e.toString()); + } + } else { + if (e.getChildren().isEmpty()) { + writeSkeleton(e.toString()); + } else { + writeSkeleton("<" + e.getName()); + List atts = e.getAttributes(); + Iterator ia = atts.iterator(); + while (ia.hasNext()) { + Attribute a = ia.next(); + writeSkeleton(" " + a.getName() + "=\"" + + Utils.cleanString(a.getValue()).replaceAll("\"", ""e;") + "\""); //$NON-NLS-3$ + } + writeSkeleton(">"); + processStory(e); + writeSkeleton(""); + } + } + } + if (n.getNodeType() == XMLNode.PROCESSING_INSTRUCTION_NODE) { + writeSkeleton(n.toString()); + } + if (n.getNodeType() == XMLNode.CDATA_SECTION_NODE) { + writeSkeleton(n.toString()); + } + } + } + + private static void processPara(Element e) throws IOException { + cleanAttributes(e); + mergeStyles(e); + writeSkeleton("<" + e.getName()); + List atts = e.getAttributes(); + Iterator ia = atts.iterator(); + while (ia.hasNext()) { + Attribute a = ia.next(); + writeSkeleton( + " " + a.getName() + "=\"" + Utils.cleanString(a.getValue()).replaceAll("\"", ""e;") + "\""); //$NON-NLS-5$ + } + writeSkeleton(">"); + String source = ""; + List content = e.getContent(); + Iterator it = content.iterator(); + boolean preamble = true; + while (it.hasNext()) { + XMLNode n = it.next(); + if (n.getNodeType() == XMLNode.TEXT_NODE) { + source = source + n.toString(); + } + if (n.getNodeType() == XMLNode.CDATA_SECTION_NODE) { + source = source + n.toString(); + } + if (n.getNodeType() == XMLNode.ELEMENT_NODE) { + Element el = (Element) n; + if (hasText(el)) { + preamble = false; + source = source + recurseElement(el); + } else { + if (preamble) { + writeSkeleton(el.toString()); + } else { + source = source + el.toString(); + } + } + } + if (n.getNodeType() == XMLNode.PROCESSING_INSTRUCTION_NODE) { + source = source + n.toString(); + } + } + source = source + ""; + String[] parts = splitCell(source); + for (int i = 0; i < parts.length; i++) { + String s = parts[i]; + String first = s.substring(0, s.indexOf("
") + 5); + writeSkeleton(first.substring("".length(), first.length() - "".length())); + if (!first.equals(s)) { + String center = s.substring(first.length(), s.lastIndexOf("")); + String last = s.substring(s.lastIndexOf("")); + String[] segments = null; + if (segmenter != null) { + segments = segmenter.segment(fixTags(center)); + } else { + segments = new String[] { fixTags(center) }; + } + for (int h = 0; h < segments.length; h++) { + if (segments[h].trim().equals("")) { + writeSkeleton(segments[h]); + } else { + writeSkeleton("%%%" + id + "%%%\n"); + writeString("\n" + segments[h] + + "\n\n"); + } + } + writeSkeleton(last.substring("".length(), last.length() - "".length())); + } + } + writeSkeleton(""); + } + + private static void mergeStyles(Element e) { + List newContent = new ArrayList<>(); + List content = e.getContent(); + Element current = null; + Element currentStyle = null; + for (int i = 0; i < content.size(); i++) { + XMLNode node = content.get(i); + if (node.getNodeType() == XMLNode.TEXT_NODE) { + newContent.add(node); + } + if (node.getNodeType() == XMLNode.ELEMENT_NODE) { + Element child = (Element) node; + if (child.getName().equals("CharacterStyleRange")) { + current = child; + currentStyle = getStyle(current); + int h = 1; + int skip = 0; + while (i + h < content.size()) { + XMLNode next = content.get(i + h); + if (next.getNodeType() == XMLNode.ELEMENT_NODE) { + Element n = (Element) next; + if (n.getName().equals("CharacterStyleRange")) { + Element nextStyle = getStyle(n); + if (currentStyle.equals(nextStyle)) { + String text = extractContent(n); + addContent(current, text); + skip = h; + } else { + break; + } + } else { + break; + } + } + h++; + } + newContent.add(node); + i = i + skip; + } else { + newContent.add(node); + } + } + } + e.setContent(newContent); + } + + private static void addContent(Element e, String text) { + Element content = e.getChild("Content"); + if (content != null) { + content.setText(content.getText() + text); + } + } + + private static String extractContent(Element e) { + Element content = e.getChild("Content"); + if (content != null) { + return content.getText(); + } + return ""; + } + + private static Element getStyle(Element e) { + Element result = new Element(e.getName()); + List atts = e.getAttributes(); + for (int i = 0; i < atts.size(); i++) { + Attribute a = atts.get(i); + result.setAttribute(a.getName(), a.getValue()); + } + List children = e.getChildren(); + Iterator it = children.iterator(); + while (it.hasNext()) { + Element child = it.next(); + if (!child.getName().equals("Content")) { + result.addContent(getStyle(child)); + } + } + return result; + } + + private static void cleanAttributes(Element e) { + Attribute a = e.getAttribute("CharacterDirection"); + if (a != null && a.getValue().equals("DefaultDirection")) { + e.removeAttribute("CharacterDirection"); + } + a = e.getAttribute("DiacriticPosition"); + if (a != null && a.getValue().equals("DefaultPosition")) { + e.removeAttribute("DiacriticPosition"); + } + a = e.getAttribute("DigitsType"); + if (a != null && a.getValue().equals("DefaultDigits")) { + e.removeAttribute("DigitsType"); + } + List children = e.getChildren(); + Iterator it = children.iterator(); + while (it.hasNext()) { + cleanAttributes(it.next()); + } + } + + private static String[] splitCell(String string) { + int index = string.indexOf(" v = new Vector<>(); + while (index != -1) { + v.add(string.substring(0, index) + ""); + string = "" + string.substring(index); + index = string.indexOf(" v2 = new Vector<>(); + for (int i = 0; i < v.size(); i++) { + String s = v.get(i); + int index2 = s.indexOf(""; + v2.add(str); + s = "" + s.substring(index2); + index2 = s.indexOf(""); + while (start != -1) { + result = result + string.substring(0, start); + string = string.substring(start); + int end = string.indexOf(""); + String clean = Utils.cleanString(string.substring("".length(), end)); + result = result + "" + clean + ""; + if (clean.indexOf('\u2028') != -1) { + result = result + "\n"; + } + string = string.substring(end + "".length()); + start = string.indexOf(""); + } + result = result + string; + return result; + } + + private static String recurseElement(Element e) { + String result = ""; + if (e.getName().equals("Content")) { + result = result + "" + Utils.cleanString(e.getText()) + ""; + } else { + result = result + "<" + e.getName(); + List atts = e.getAttributes(); + Iterator ia = atts.iterator(); + while (ia.hasNext()) { + Attribute a = ia.next(); + result = result + " " + a.getName() + "=\"" + + Utils.cleanString(a.getValue()).replaceAll("\"", ""e;") + "\""; //$NON-NLS-3$ + } + if (e.getContent().isEmpty()) { + result = result + " />"; + } else { + result = result + ">"; + List content = e.getContent(); + Iterator it = content.iterator(); + while (it.hasNext()) { + XMLNode n = it.next(); + if (n.getNodeType() == XMLNode.TEXT_NODE) { + result = result + n.toString(); + } + if (n.getNodeType() == XMLNode.ELEMENT_NODE) { + result = result + recurseElement((Element) n); + } + } + result = result + ""; + } + } + return result; + } + + private static boolean hasText(Element e) { + if (e.getName().equals("Content") && !e.getText().isEmpty()) { + return true; + } + List children = e.getChildren(); + Iterator it = children.iterator(); + while (it.hasNext()) { + if (hasText(it.next())) { + return true; + } + } + return false; + } + + private static void writeHeader() throws IOException { + String tgtLang = ""; + if (targetLanguage != null) { + tgtLang = "\" target-language=\"" + targetLanguage; + } + String format = "x-idml"; + writeString("\n"); + writeString("\n"); + writeString("\n"); + + writeString("\n"); + writeString("
\n"); + writeString(" \n"); + writeString(" \n"); + writeString(" \n"); + writeString("
\n"); + writeString("\n"); + } + + private static void writeString(String string) throws IOException { + output.write(string.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/src/com/maxprograms/converters/idml/Xliff2Idml.java b/src/com/maxprograms/converters/idml/Xliff2Idml.java index ec2a9b64..4291668d 100644 --- a/src/com/maxprograms/converters/idml/Xliff2Idml.java +++ b/src/com/maxprograms/converters/idml/Xliff2Idml.java @@ -1,190 +1,191 @@ -/******************************************************************************* - * Copyright (c) 2003-2019 Maxprograms. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 1.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/org/documents/epl-v10.html - * - * Contributors: - * Maxprograms - initial API and implementation - *******************************************************************************/ -package com.maxprograms.converters.idml; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.List; -import java.util.Vector; -import java.lang.System.Logger.Level; -import java.lang.System.Logger; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; -import java.util.zip.ZipOutputStream; - -import javax.xml.parsers.ParserConfigurationException; - -import org.xml.sax.SAXException; - -import com.maxprograms.converters.xml.Xliff2Xml; -import com.maxprograms.xml.Document; -import com.maxprograms.xml.Element; -import com.maxprograms.xml.SAXBuilder; -import com.maxprograms.xml.XMLOutputter; - -public class Xliff2Idml { - - private static Hashtable filesTable; - private static boolean isEmbedded; - - private Xliff2Idml() { - // do not instantiate this class - // use run method instead - } - - public static Vector run(Hashtable params) { - Vector result = new Vector<>(); - String xliffFile = params.get("xliff"); - String outputFile = params.get("backfile"); - filesTable = new Hashtable<>(); - try { - SAXBuilder builder = new SAXBuilder(); - Document doc = builder.build(xliffFile); - Element root = doc.getRootElement(); - List files = root.getChildren("file"); - Iterator it = files.iterator(); - while (it.hasNext()) { - saveFile(it.next(), xliffFile); - } - String skeleton = params.get("skeleton"); - if (isEmbedded) { - File t = new File(skeleton); - t.deleteOnExit(); - } - - try (ZipInputStream in = new ZipInputStream(new FileInputStream(skeleton))) { - File f = new File(outputFile); - File p = f.getParentFile(); - if (p == null) { - p = new File(System.getProperty("user.dir")); - } - if (!p.exists()) { - p.mkdirs(); - } - if (!f.exists()) { - Files.createFile(Paths.get(f.toURI())); - } - try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(f))) { - ZipEntry entry = null; - while ((entry = in.getNextEntry()) != null) { - if (entry.getName().matches(".*Story_.*\\.xml\\.skl")) { - String name = entry.getName().substring(0, entry.getName().lastIndexOf(".skl")); - File tmp = new File(filesTable.get(name) + ".skl"); - try (FileOutputStream output = new FileOutputStream(tmp.getAbsolutePath())) { - byte[] buf = new byte[1024]; - int len; - while ((len = in.read(buf)) > 0) { - output.write(buf, 0, len); - } - } - Hashtable table = new Hashtable<>(); - table.put("xliff", filesTable.get(name)); - table.put("backfile", filesTable.get(name) + ".xml"); - table.put("catalog", params.get("catalog")); - table.put("skeleton", filesTable.get(name) + ".skl"); - table.put("encoding", params.get("encoding")); - table.put("IDML", "true"); - Vector res = Xliff2Xml.run(table); - if (!"0".equals(res.get(0))) { - return res; - } - ZipEntry content = new ZipEntry(name); - content.setMethod(ZipEntry.DEFLATED); - out.putNextEntry(content); - try (FileInputStream input = new FileInputStream(filesTable.get(name) + ".xml")) { - byte[] buf = new byte[1024]; - int len; - while ((len = input.read(buf)) > 0) { - out.write(buf, 0, len); - } - out.closeEntry(); - } - File xml = new File(filesTable.get(name) + ".xml"); - Files.delete(Paths.get(xml.toURI())); - File xlf = new File(filesTable.get(name)); - Files.delete(Paths.get(xlf.toURI())); - tmp.deleteOnExit(); - } else { - File tmp = File.createTempFile("entry", ".tmp"); - try (FileOutputStream output = new FileOutputStream(tmp.getAbsolutePath())) { - byte[] buf = new byte[1024]; - int len; - while ((len = in.read(buf)) > 0) { - output.write(buf, 0, len); - } - } - - ZipEntry content = new ZipEntry(entry.getName()); - content.setMethod(ZipEntry.DEFLATED); - out.putNextEntry(content); - try (FileInputStream input = new FileInputStream(tmp.getAbsolutePath())) { - byte[] buf = new byte[1024]; - int len; - while ((len = input.read(buf)) > 0) { - out.write(buf, 0, len); - } - out.closeEntry(); - } - tmp.deleteOnExit(); - } - } - } - } - if (isEmbedded) { - File f1 = new File(skeleton); - Files.delete(Paths.get(f1.toURI())); - } - result.add("0"); - } catch (IOException | SAXException | ParserConfigurationException e) { - Logger logger = System.getLogger(Xliff2Idml.class.getName()); - logger.log(Level.ERROR, "Error merging IDML file", e); - result.add("1"); - result.add(e.getMessage()); - } - return result; - } - - private static void saveFile(Element element, String xliffFile) throws IOException { - Document doc = new Document(null, "xliff", null, null); - Element root = doc.getRootElement(); - root.setAttribute("version", "1.2"); - Element file = new Element("file"); - file.clone(element); - root.addContent(file); - File xliff = File.createTempFile("tmp", ".xlf", new File(xliffFile).getParentFile()); - List groups = file.getChild("header").getChildren("prop-group"); - Iterator i = groups.iterator(); - while (i.hasNext()) { - Element group = i.next(); - if (group.getAttributeValue("name").equals("document")) { - filesTable.put(group.getChild("prop").getText(), xliff.getAbsolutePath()); - } - } - if (file.getChild("header").getChild("skl").getChild("external-file") == null) { - // embedded skeleton - file.getChild("header").getChild("skl").addContent(new Element("external-file")); - isEmbedded = true; - } - file.getChild("header").getChild("skl").getChild("external-file").setAttribute("href", - xliff.getAbsolutePath() + ".skl"); //$NON-NLS-1$ - XMLOutputter outputter = new XMLOutputter(); - try (FileOutputStream output = new FileOutputStream(xliff.getAbsolutePath())) { - outputter.output(doc, output); - } - } -} +/******************************************************************************* + * Copyright (c) 2003-2019 Maxprograms. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/org/documents/epl-v10.html + * + * Contributors: + * Maxprograms - initial API and implementation + *******************************************************************************/ +package com.maxprograms.converters.idml; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Vector; +import java.lang.System.Logger.Level; +import java.lang.System.Logger; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +import javax.xml.parsers.ParserConfigurationException; + +import org.xml.sax.SAXException; + +import com.maxprograms.converters.Constants; +import com.maxprograms.converters.xml.Xliff2Xml; +import com.maxprograms.xml.Document; +import com.maxprograms.xml.Element; +import com.maxprograms.xml.SAXBuilder; +import com.maxprograms.xml.XMLOutputter; + +public class Xliff2Idml { + + private static Hashtable filesTable; + private static boolean isEmbedded; + + private Xliff2Idml() { + // do not instantiate this class + // use run method instead + } + + public static Vector run(Hashtable params) { + Vector result = new Vector<>(); + String xliffFile = params.get("xliff"); + String outputFile = params.get("backfile"); + filesTable = new Hashtable<>(); + try { + SAXBuilder builder = new SAXBuilder(); + Document doc = builder.build(xliffFile); + Element root = doc.getRootElement(); + List files = root.getChildren("file"); + Iterator it = files.iterator(); + while (it.hasNext()) { + saveFile(it.next(), xliffFile); + } + String skeleton = params.get("skeleton"); + if (isEmbedded) { + File t = new File(skeleton); + t.deleteOnExit(); + } + + try (ZipInputStream in = new ZipInputStream(new FileInputStream(skeleton))) { + File f = new File(outputFile); + File p = f.getParentFile(); + if (p == null) { + p = new File(System.getProperty("user.dir")); + } + if (!p.exists()) { + p.mkdirs(); + } + if (!f.exists()) { + Files.createFile(Paths.get(f.toURI())); + } + try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(f))) { + ZipEntry entry = null; + while ((entry = in.getNextEntry()) != null) { + if (entry.getName().matches(".*Story_.*\\.xml\\.skl")) { + String name = entry.getName().substring(0, entry.getName().lastIndexOf(".skl")); + File tmp = new File(filesTable.get(name) + ".skl"); + try (FileOutputStream output = new FileOutputStream(tmp.getAbsolutePath())) { + byte[] buf = new byte[1024]; + int len; + while ((len = in.read(buf)) > 0) { + output.write(buf, 0, len); + } + } + Hashtable table = new Hashtable<>(); + table.put("xliff", filesTable.get(name)); + table.put("backfile", filesTable.get(name) + ".xml"); + table.put("catalog", params.get("catalog")); + table.put("skeleton", filesTable.get(name) + ".skl"); + table.put("encoding", params.get("encoding")); + table.put("IDML", "true"); + Vector res = Xliff2Xml.run(table); + if (!Constants.SUCCESS.equals(res.get(0))) { + return res; + } + ZipEntry content = new ZipEntry(name); + content.setMethod(ZipEntry.DEFLATED); + out.putNextEntry(content); + try (FileInputStream input = new FileInputStream(filesTable.get(name) + ".xml")) { + byte[] buf = new byte[1024]; + int len; + while ((len = input.read(buf)) > 0) { + out.write(buf, 0, len); + } + out.closeEntry(); + } + File xml = new File(filesTable.get(name) + ".xml"); + Files.delete(Paths.get(xml.toURI())); + File xlf = new File(filesTable.get(name)); + Files.delete(Paths.get(xlf.toURI())); + tmp.deleteOnExit(); + } else { + File tmp = File.createTempFile("entry", ".tmp"); + try (FileOutputStream output = new FileOutputStream(tmp.getAbsolutePath())) { + byte[] buf = new byte[1024]; + int len; + while ((len = in.read(buf)) > 0) { + output.write(buf, 0, len); + } + } + + ZipEntry content = new ZipEntry(entry.getName()); + content.setMethod(ZipEntry.DEFLATED); + out.putNextEntry(content); + try (FileInputStream input = new FileInputStream(tmp.getAbsolutePath())) { + byte[] buf = new byte[1024]; + int len; + while ((len = input.read(buf)) > 0) { + out.write(buf, 0, len); + } + out.closeEntry(); + } + tmp.deleteOnExit(); + } + } + } + } + if (isEmbedded) { + File f1 = new File(skeleton); + Files.delete(Paths.get(f1.toURI())); + } + result.add(Constants.SUCCESS); + } catch (IOException | SAXException | ParserConfigurationException e) { + Logger logger = System.getLogger(Xliff2Idml.class.getName()); + logger.log(Level.ERROR, "Error merging IDML file", e); + result.add(Constants.ERROR); + result.add(e.getMessage()); + } + return result; + } + + private static void saveFile(Element element, String xliffFile) throws IOException { + Document doc = new Document(null, "xliff", null, null); + Element root = doc.getRootElement(); + root.setAttribute("version", "1.2"); + Element file = new Element("file"); + file.clone(element); + root.addContent(file); + File xliff = File.createTempFile("tmp", ".xlf", new File(xliffFile).getParentFile()); + List groups = file.getChild("header").getChildren("prop-group"); + Iterator i = groups.iterator(); + while (i.hasNext()) { + Element group = i.next(); + if (group.getAttributeValue("name").equals("document")) { + filesTable.put(group.getChild("prop").getText(), xliff.getAbsolutePath()); + } + } + if (file.getChild("header").getChild("skl").getChild("external-file") == null) { + // embedded skeleton + file.getChild("header").getChild("skl").addContent(new Element("external-file")); + isEmbedded = true; + } + file.getChild("header").getChild("skl").getChild("external-file").setAttribute("href", + xliff.getAbsolutePath() + ".skl"); //$NON-NLS-1$ + XMLOutputter outputter = new XMLOutputter(); + try (FileOutputStream output = new FileOutputStream(xliff.getAbsolutePath())) { + outputter.output(doc, output); + } + } +} diff --git a/src/com/maxprograms/converters/javaproperties/Properties2Xliff.java b/src/com/maxprograms/converters/javaproperties/Properties2Xliff.java index e14780cb..23638d86 100644 --- a/src/com/maxprograms/converters/javaproperties/Properties2Xliff.java +++ b/src/com/maxprograms/converters/javaproperties/Properties2Xliff.java @@ -1,205 +1,206 @@ -/******************************************************************************* - * Copyright (c) 2003-2019 Maxprograms. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 1.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/org/documents/epl-v10.html - * - * Contributors: - * Maxprograms - initial API and implementation - *******************************************************************************/ -package com.maxprograms.converters.javaproperties; - -import java.io.BufferedReader; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.util.Hashtable; -import java.util.Vector; -import java.lang.System.Logger.Level; -import java.lang.System.Logger; - -import javax.xml.parsers.ParserConfigurationException; - -import org.xml.sax.SAXException; - -import com.maxprograms.converters.Utils; -import com.maxprograms.segmenter.Segmenter; - -public class Properties2Xliff { - - private static FileOutputStream output; - private static FileOutputStream skeleton; - private static String source; - private static String sourceLanguage; - private static int segId; - private static Segmenter segmenter; - private static boolean segByElement; - - private Properties2Xliff() { - // do not instantiate this class - // use run method instead - } - - public static Vector run(Hashtable params) { - Vector result = new Vector<>(); - segId = 0; - - String inputFile = params.get("source"); - String xliffFile = params.get("xliff"); - String skeletonFile = params.get("skeleton"); - sourceLanguage = params.get("srcLang"); - String targetLanguage = params.get("tgtLang"); - String srcEncoding = params.get("srcEncoding"); - String elementSegmentation = params.get("paragraph"); - String catalog = params.get("catalog"); - - if (elementSegmentation == null) { - segByElement = false; - } else { - if (elementSegmentation.equals("yes")) { - segByElement = true; - } else { - segByElement = false; - } - } - - source = ""; - try { - if (!segByElement) { - String initSegmenter = params.get("srxFile"); - segmenter = new Segmenter(initSegmenter, sourceLanguage, catalog); - } - FileInputStream stream = new FileInputStream(inputFile); - try (InputStreamReader input = new InputStreamReader(stream, srcEncoding)) { - BufferedReader buffer = new BufferedReader(input); - output = new FileOutputStream(xliffFile); - String tgtLang = ""; - if (targetLanguage != null) { - tgtLang = "\" target-language=\"" + targetLanguage; - } - - writeString("\n"); - writeString("\n"); - writeString("\n"); - - writeString("\n"); - writeString("
\n"); - writeString(" \n"); - writeString(" \n"); - writeString(" \n"); - writeString("
\n"); - writeString("\n"); - - skeleton = new FileOutputStream(skeletonFile); - - String line; - while ((line = buffer.readLine()) != null) { - - if (line.trim().length() == 0) { - // no text in this line - // segment separator - writeSkeleton(line + "\n"); - } else if (line.trim().startsWith("#")) { - // this line is a comment - // send to skeleton - writeSkeleton(line + "\n"); - } else { - String tmp = line; - if (line.endsWith("\\")) { - do { - line = buffer.readLine(); - tmp += "\n" + line; - } while (line != null && line.endsWith("\\")); - } - int index = tmp.indexOf('='); - if (index != -1) { - String key = tmp.substring(0, index + 1); - writeSkeleton(key); - source = tmp.substring(index + 1); - writeSegment(); - writeSkeleton("\n"); - } else { - // this line may be wrong, send to skeleton - // and continue - writeSkeleton(tmp); - } - } - } - - skeleton.close(); - - writeString("\n"); - writeString("
\n"); - writeString("
"); - } - output.close(); - - result.add("0"); // success - } catch (IOException | SAXException | ParserConfigurationException e) { - Logger logger = System.getLogger(Properties2Xliff.class.getName()); - logger.log(Level.ERROR, "Error converting .properties file", e); - result.add("1"); - result.add(e.getMessage()); - } - - return result; - } - - private static void writeString(String string) throws IOException { - output.write(string.getBytes(StandardCharsets.UTF_8)); - } - - private static void writeSkeleton(String string) throws IOException { - skeleton.write(string.getBytes(StandardCharsets.UTF_8)); - } - - private static void writeSegment() throws IOException { - String[] segments; - if (!segByElement) { - segments = segmenter.segment(fixChars(source)); - } else { - segments = new String[1]; - segments[0] = fixChars(source); - } - for (int i = 0; i < segments.length; i++) { - if (!segments[i].trim().equals("")) { - writeString(" \n" - + " " + Utils.cleanString(segments[i]) - + "\n"); - writeString(" \n"); - writeSkeleton("%%%" + segId++ + "%%%"); - } else { - writeSkeleton(segments[i]); - } - } - source = ""; - } - - private static String fixChars(String string) { - String result = string; - int start = result.indexOf("\\u"); - while (start != -1) { - if (result.substring(start + 2, start + 6).toLowerCase() - .matches("[\\dabcdef][\\dabcdef][\\dabcdef][\\dabcdef]")) { - result = result.substring(0, start) + toChar(result.substring(start + 2, start + 6)) - + result.substring(start + 6); - } - start = result.indexOf("\\u", start + 1); - } - return result; - } - - private static String toChar(String string) { - int hex = Integer.parseInt(string, 16); - char result = (char) hex; - return "" + result; - } - -} +/******************************************************************************* + * Copyright (c) 2003-2019 Maxprograms. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/org/documents/epl-v10.html + * + * Contributors: + * Maxprograms - initial API and implementation + *******************************************************************************/ +package com.maxprograms.converters.javaproperties; + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Hashtable; +import java.util.Vector; +import java.lang.System.Logger.Level; +import java.lang.System.Logger; + +import javax.xml.parsers.ParserConfigurationException; + +import org.xml.sax.SAXException; + +import com.maxprograms.converters.Constants; +import com.maxprograms.converters.Utils; +import com.maxprograms.segmenter.Segmenter; + +public class Properties2Xliff { + + private static FileOutputStream output; + private static FileOutputStream skeleton; + private static String source; + private static String sourceLanguage; + private static int segId; + private static Segmenter segmenter; + private static boolean segByElement; + + private Properties2Xliff() { + // do not instantiate this class + // use run method instead + } + + public static Vector run(Hashtable params) { + Vector result = new Vector<>(); + segId = 0; + + String inputFile = params.get("source"); + String xliffFile = params.get("xliff"); + String skeletonFile = params.get("skeleton"); + sourceLanguage = params.get("srcLang"); + String targetLanguage = params.get("tgtLang"); + String srcEncoding = params.get("srcEncoding"); + String elementSegmentation = params.get("paragraph"); + String catalog = params.get("catalog"); + + if (elementSegmentation == null) { + segByElement = false; + } else { + if (elementSegmentation.equals("yes")) { + segByElement = true; + } else { + segByElement = false; + } + } + + source = ""; + try { + if (!segByElement) { + String initSegmenter = params.get("srxFile"); + segmenter = new Segmenter(initSegmenter, sourceLanguage, catalog); + } + FileInputStream stream = new FileInputStream(inputFile); + try (InputStreamReader input = new InputStreamReader(stream, srcEncoding)) { + BufferedReader buffer = new BufferedReader(input); + output = new FileOutputStream(xliffFile); + String tgtLang = ""; + if (targetLanguage != null) { + tgtLang = "\" target-language=\"" + targetLanguage; + } + + writeString("\n"); + writeString("\n"); + writeString("\n"); + + writeString("\n"); + writeString("
\n"); + writeString(" \n"); + writeString(" \n"); + writeString(" \n"); + writeString("
\n"); + writeString("\n"); + + skeleton = new FileOutputStream(skeletonFile); + + String line; + while ((line = buffer.readLine()) != null) { + + if (line.trim().length() == 0) { + // no text in this line + // segment separator + writeSkeleton(line + "\n"); + } else if (line.trim().startsWith("#")) { + // this line is a comment + // send to skeleton + writeSkeleton(line + "\n"); + } else { + String tmp = line; + if (line.endsWith("\\")) { + do { + line = buffer.readLine(); + tmp += "\n" + line; + } while (line != null && line.endsWith("\\")); + } + int index = tmp.indexOf('='); + if (index != -1) { + String key = tmp.substring(0, index + 1); + writeSkeleton(key); + source = tmp.substring(index + 1); + writeSegment(); + writeSkeleton("\n"); + } else { + // this line may be wrong, send to skeleton + // and continue + writeSkeleton(tmp); + } + } + } + + skeleton.close(); + + writeString("\n"); + writeString("
\n"); + writeString("
"); + } + output.close(); + + result.add(Constants.SUCCESS); + } catch (IOException | SAXException | ParserConfigurationException e) { + Logger logger = System.getLogger(Properties2Xliff.class.getName()); + logger.log(Level.ERROR, "Error converting .properties file", e); + result.add(Constants.ERROR); + result.add(e.getMessage()); + } + + return result; + } + + private static void writeString(String string) throws IOException { + output.write(string.getBytes(StandardCharsets.UTF_8)); + } + + private static void writeSkeleton(String string) throws IOException { + skeleton.write(string.getBytes(StandardCharsets.UTF_8)); + } + + private static void writeSegment() throws IOException { + String[] segments; + if (!segByElement) { + segments = segmenter.segment(fixChars(source)); + } else { + segments = new String[1]; + segments[0] = fixChars(source); + } + for (int i = 0; i < segments.length; i++) { + if (!segments[i].trim().equals("")) { + writeString(" \n" + + " " + Utils.cleanString(segments[i]) + + "\n"); + writeString(" \n"); + writeSkeleton("%%%" + segId++ + "%%%"); + } else { + writeSkeleton(segments[i]); + } + } + source = ""; + } + + private static String fixChars(String string) { + String result = string; + int start = result.indexOf("\\u"); + while (start != -1) { + if (result.substring(start + 2, start + 6).toLowerCase() + .matches("[\\dabcdef][\\dabcdef][\\dabcdef][\\dabcdef]")) { + result = result.substring(0, start) + toChar(result.substring(start + 2, start + 6)) + + result.substring(start + 6); + } + start = result.indexOf("\\u", start + 1); + } + return result; + } + + private static String toChar(String string) { + int hex = Integer.parseInt(string, 16); + char result = (char) hex; + return "" + result; + } + +} diff --git a/src/com/maxprograms/converters/javaproperties/Xliff2Properties.java b/src/com/maxprograms/converters/javaproperties/Xliff2Properties.java index 11b2ce40..54269c08 100644 --- a/src/com/maxprograms/converters/javaproperties/Xliff2Properties.java +++ b/src/com/maxprograms/converters/javaproperties/Xliff2Properties.java @@ -1,206 +1,207 @@ -/******************************************************************************* - * Copyright (c) 2003-2019 Maxprograms. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 1.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/org/documents/epl-v10.html - * - * Contributors: - * Maxprograms - initial API and implementation - *******************************************************************************/ -package com.maxprograms.converters.javaproperties; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.text.MessageFormat; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.List; -import java.util.Vector; -import java.lang.System.Logger.Level; -import java.lang.System.Logger; - -import javax.xml.parsers.ParserConfigurationException; - -import org.xml.sax.SAXException; - -import com.maxprograms.xml.Catalog; -import com.maxprograms.xml.Document; -import com.maxprograms.xml.Element; -import com.maxprograms.xml.SAXBuilder; -import com.maxprograms.xml.TextNode; -import com.maxprograms.xml.XMLNode; - -public class Xliff2Properties { - - private static String xliffFile; - private static String encoding; - private static Hashtable segments; - private static Catalog catalog; - private static FileOutputStream output; - - private Xliff2Properties() { - // do not instantiate this class - // use run method instead - } - - public static Vector run(Hashtable params) { - - Vector result = new Vector<>(); - - String sklFile = params.get("skeleton"); - xliffFile = params.get("xliff"); - encoding = params.get("encoding"); - - try { - catalog = new Catalog(params.get("catalog")); - String outputFile = params.get("backfile"); - File f = new File(outputFile); - File p = f.getParentFile(); - if (p == null) { - p = new File(System.getProperty("user.dir")); - } - if (!p.exists()) { - p.mkdirs(); - } - if (!f.exists()) { - Files.createFile(Paths.get(f.toURI())); - } - output = new FileOutputStream(f); - loadSegments(); - - try (InputStreamReader input = new InputStreamReader(new FileInputStream(sklFile), - StandardCharsets.UTF_8)) { - BufferedReader buffer = new BufferedReader(input); - String line; - while ((line = buffer.readLine()) != null) { - line = line + "\n"; - - if (line.indexOf("%%%") != -1) { - // - // contains translatable text - // - int index = line.indexOf("%%%"); - while (index != -1) { - String start = line.substring(0, index); - writeString(start); - line = line.substring(index + 3); - String code = line.substring(0, line.indexOf("%%%")); - line = line.substring(line.indexOf("%%%") + 3); - Element segment = segments.get(code); - if (segment != null) { - Element target = segment.getChild("target"); - Element source = segment.getChild("source"); - if (target != null) { - if (segment.getAttributeValue("approved", "no").equalsIgnoreCase("yes")) { - writeString(extractText(target)); - } else { - writeString(extractText(source)); - } - } else { - writeString(extractText(source)); - } - } else { - result.add(0, "1"); - MessageFormat mf = new MessageFormat("Segment {0} not found."); - result.add(1, mf.format(new Object[] { code })); - return result; - } - - index = line.indexOf("%%%"); - if (index == -1) { - writeString(line); - } - } // end while - } else { - // - // non translatable portion - // - writeString(line); - } - } - } - output.close(); - result.add("0"); - } catch (IOException | SAXException | ParserConfigurationException e) { - Logger logger = System.getLogger(Xliff2Properties.class.getName()); - logger.log(Level.ERROR, "Error merging .properties file.", e); - result.add("1"); - result.add(e.getMessage()); - } - return result; - } - - private static String extractText(Element target) { - String result = ""; - List content = target.getContent(); - Iterator i = content.iterator(); - while (i.hasNext()) { - XMLNode n = i.next(); - if (n.getNodeType() == XMLNode.ELEMENT_NODE) { - Element e = (Element) n; - result = result + extractText(e); - } - if (n.getNodeType() == XMLNode.TEXT_NODE) { - result = result + ((TextNode) n).getText(); - } - } - return cleanChars(result); - } - - private static String cleanChars(String string) { - String result = ""; - int size = string.length(); - for (int i = 0; i < size; i++) { - char c = string.charAt(i); - if (c <= 255) { - result = result + c; - } else { - result = result + toHex(c); - } - } - return result; - } - - private static String toHex(char c) { - String hex = Integer.toHexString(c); - while (hex.length() < 4) { - hex = "0" + hex; - } - return "\\u" + hex; - } - - private static void loadSegments() throws SAXException, IOException, ParserConfigurationException { - - SAXBuilder builder = new SAXBuilder(); - if (catalog != null) { - builder.setEntityResolver(catalog); - } - - Document doc = builder.build(xliffFile); - Element root = doc.getRootElement(); - Element body = root.getChild("file").getChild("body"); - List units = body.getChildren("trans-unit"); - Iterator i = units.iterator(); - - segments = new Hashtable<>(); - - while (i.hasNext()) { - Element unit = i.next(); - segments.put(unit.getAttributeValue("id"), unit); - } - } - - private static void writeString(String string) throws IOException { - output.write(string.getBytes(encoding)); - } - -} +/******************************************************************************* + * Copyright (c) 2003-2019 Maxprograms. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/org/documents/epl-v10.html + * + * Contributors: + * Maxprograms - initial API and implementation + *******************************************************************************/ +package com.maxprograms.converters.javaproperties; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.text.MessageFormat; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Vector; +import java.lang.System.Logger.Level; +import java.lang.System.Logger; + +import javax.xml.parsers.ParserConfigurationException; + +import org.xml.sax.SAXException; + +import com.maxprograms.converters.Constants; +import com.maxprograms.xml.Catalog; +import com.maxprograms.xml.Document; +import com.maxprograms.xml.Element; +import com.maxprograms.xml.SAXBuilder; +import com.maxprograms.xml.TextNode; +import com.maxprograms.xml.XMLNode; + +public class Xliff2Properties { + + private static String xliffFile; + private static String encoding; + private static Hashtable segments; + private static Catalog catalog; + private static FileOutputStream output; + + private Xliff2Properties() { + // do not instantiate this class + // use run method instead + } + + public static Vector run(Hashtable params) { + + Vector result = new Vector<>(); + + String sklFile = params.get("skeleton"); + xliffFile = params.get("xliff"); + encoding = params.get("encoding"); + + try { + catalog = new Catalog(params.get("catalog")); + String outputFile = params.get("backfile"); + File f = new File(outputFile); + File p = f.getParentFile(); + if (p == null) { + p = new File(System.getProperty("user.dir")); + } + if (!p.exists()) { + p.mkdirs(); + } + if (!f.exists()) { + Files.createFile(Paths.get(f.toURI())); + } + output = new FileOutputStream(f); + loadSegments(); + + try (InputStreamReader input = new InputStreamReader(new FileInputStream(sklFile), + StandardCharsets.UTF_8)) { + BufferedReader buffer = new BufferedReader(input); + String line; + while ((line = buffer.readLine()) != null) { + line = line + "\n"; + + if (line.indexOf("%%%") != -1) { + // + // contains translatable text + // + int index = line.indexOf("%%%"); + while (index != -1) { + String start = line.substring(0, index); + writeString(start); + line = line.substring(index + 3); + String code = line.substring(0, line.indexOf("%%%")); + line = line.substring(line.indexOf("%%%") + 3); + Element segment = segments.get(code); + if (segment != null) { + Element target = segment.getChild("target"); + Element source = segment.getChild("source"); + if (target != null) { + if (segment.getAttributeValue("approved", "no").equalsIgnoreCase("yes")) { + writeString(extractText(target)); + } else { + writeString(extractText(source)); + } + } else { + writeString(extractText(source)); + } + } else { + result.add(Constants.ERROR); + MessageFormat mf = new MessageFormat("Segment {0} not found."); + result.add(mf.format(new Object[] { code })); + return result; + } + + index = line.indexOf("%%%"); + if (index == -1) { + writeString(line); + } + } // end while + } else { + // + // non translatable portion + // + writeString(line); + } + } + } + output.close(); + result.add(Constants.SUCCESS); + } catch (IOException | SAXException | ParserConfigurationException e) { + Logger logger = System.getLogger(Xliff2Properties.class.getName()); + logger.log(Level.ERROR, "Error merging .properties file.", e); + result.add(Constants.ERROR); + result.add(e.getMessage()); + } + return result; + } + + private static String extractText(Element target) { + String result = ""; + List content = target.getContent(); + Iterator i = content.iterator(); + while (i.hasNext()) { + XMLNode n = i.next(); + if (n.getNodeType() == XMLNode.ELEMENT_NODE) { + Element e = (Element) n; + result = result + extractText(e); + } + if (n.getNodeType() == XMLNode.TEXT_NODE) { + result = result + ((TextNode) n).getText(); + } + } + return cleanChars(result); + } + + private static String cleanChars(String string) { + String result = ""; + int size = string.length(); + for (int i = 0; i < size; i++) { + char c = string.charAt(i); + if (c <= 255) { + result = result + c; + } else { + result = result + toHex(c); + } + } + return result; + } + + private static String toHex(char c) { + String hex = Integer.toHexString(c); + while (hex.length() < 4) { + hex = "0" + hex; + } + return "\\u" + hex; + } + + private static void loadSegments() throws SAXException, IOException, ParserConfigurationException { + + SAXBuilder builder = new SAXBuilder(); + if (catalog != null) { + builder.setEntityResolver(catalog); + } + + Document doc = builder.build(xliffFile); + Element root = doc.getRootElement(); + Element body = root.getChild("file").getChild("body"); + List units = body.getChildren("trans-unit"); + Iterator i = units.iterator(); + + segments = new Hashtable<>(); + + while (i.hasNext()) { + Element unit = i.next(); + segments.put(unit.getAttributeValue("id"), unit); + } + } + + private static void writeString(String string) throws IOException { + output.write(string.getBytes(encoding)); + } + +} diff --git a/src/com/maxprograms/converters/javascript/Jscript2xliff.java b/src/com/maxprograms/converters/javascript/Jscript2xliff.java index 9db03a21..db43c1e9 100644 --- a/src/com/maxprograms/converters/javascript/Jscript2xliff.java +++ b/src/com/maxprograms/converters/javascript/Jscript2xliff.java @@ -1,244 +1,245 @@ -/******************************************************************************* - * Copyright (c) 2003-2019 Maxprograms. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 1.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/org/documents/epl-v10.html - * - * Contributors: - * Maxprograms - initial API and implementation - *******************************************************************************/ -package com.maxprograms.converters.javascript; - -import java.io.BufferedReader; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.util.Hashtable; -import java.util.Vector; -import java.lang.System.Logger.Level; -import java.lang.System.Logger; - -import com.maxprograms.converters.Utils; - -public class Jscript2xliff { - - private static FileOutputStream output; - private static FileOutputStream skeleton; - private static String sourceLanguage; - private static int segId; - - private Jscript2xliff() { - // do not instantiate this class - // use run method instead - } - - public static Vector run(Hashtable params) { - Vector result = new Vector<>(); - - String inputFile = params.get("source"); - String xliffFile = params.get("xliff"); - String skeletonFile = params.get("skeleton"); - sourceLanguage = params.get("srcLang"); - String targetLanguage = params.get("tgtLang"); - String encoding = params.get("srcEncoding"); - String tgtLang = ""; - if (targetLanguage != null) { - tgtLang = "\" target-language=\"" + targetLanguage; - } - - try { - try (FileInputStream stream = new FileInputStream(inputFile)) { - try (InputStreamReader input = new InputStreamReader(stream, encoding)) { - BufferedReader buffer = new BufferedReader(input); - output = new FileOutputStream(xliffFile); - - writeString("\n"); - writeString("\n"); - writeString("\n"); - writeString("\n"); - writeString("
\n"); - writeString(" \n"); - writeString(" \n"); - writeString(" \n"); - writeString("
\n"); - writeString("\n"); - - skeleton = new FileOutputStream(skeletonFile); - - String line; - String comment = ""; - while ((line = buffer.readLine()) != null) { - line = line + "\n"; - comment = findComment(line); - if (!comment.equals("")) { - line = line.substring(0, line.indexOf(comment)); - } - - // TODO: check for /* block comments */ - - if (line.indexOf('\"') == -1 && line.indexOf('\'') == -1) { - // no text in this line - writeSkeleton(line + comment); - } else { - // check for strings to extract - int number = countQuotes(line, '\"') + countQuotes(line, '\''); - if (number > 0 && number % 2 == 0) { - // all strings closed in the same line - extractStrings(line); - writeSkeleton(comment); - } else { - // check if the line ends with "/" - if (line.trim().endsWith("/") && !line.trim().endsWith("//")) { - String nextLine = buffer.readLine(); - if (nextLine == null) { - result.add(0, "1"); - result.add(1, "Unexpected end of file."); - return result; - } - comment = findComment(nextLine); - if (!comment.equals("")) { - nextLine = nextLine.substring(0, nextLine.indexOf(comment)); - } - line = line + nextLine; - continue; - } - result.add(0, "1"); - result.add(1, "Found a string that is not properly closed."); - return result; - } - } - } - - skeleton.close(); - - writeString("\n"); - writeString("
\n"); - writeString("
"); - } - } - output.close(); - result.add("0"); // success - } catch (IOException e) { - Logger logger = System.getLogger(Jscript2xliff.class.getName()); - logger.log(Level.ERROR, "Error converting JavaScript file.", e); - result.add("1"); - result.add(e.getMessage()); - } - return result; - } - - private static String findComment(String line) { - boolean inString = false; - for (int i = 0; i < line.length(); i++) { - char c = line.charAt(i); - if (c == '\"' || c == '\'') { - if (i > 0) { - // not at start of line - if (inString) { - if (line.charAt(i - 1) == '\\') { - // escaped quote, ignore - } else { - // close the string - inString = false; - } - } else { - // string starts here - inString = true; - } - } else { - // start of line - inString = true; - } - } - if (!inString && c == '/' && i + 1 < line.length() && line.charAt(i + 1) == '/') { - // it's a comment! - return line.substring(i); - } - } - return ""; - } - - private static void extractStrings(String line) throws IOException { - while (line.length() > 0) { - line = checkForQuote(line, '\"'); - line = checkForQuote(line, '\''); - if (line.indexOf('\"') == -1 && line.indexOf('\'') == -1) { - // no more quoted sections in the string - writeSkeleton(line); - line = ""; - } - } // line length > 0 ? - } - - private static String checkForQuote(String line, char c) throws IOException { - boolean isString = true; - int index = line.indexOf(c); - if (index > 0 && line.charAt(index - 1) == '\\') { - isString = false; - } - if (isString) { - String start = line.substring(0, index + 1); - writeSkeleton(start); - line = line.substring(index + 1); - StringBuilder buff = new StringBuilder(); - for (int i = 0; i < line.length(); i++) { - if (line.charAt(i) == c) { - boolean endsString = true; - if (i > 0 && line.charAt(i - 1) == '\\') { - endsString = false; - } - if (endsString) { - writeSegment(buff.toString()); - line = line.substring(buff.toString().length() + 1); - writeSkeleton("" + c); - break; - } - } - buff.append(line.charAt(i)); - } - } - return line; - } - - private static void writeSegment(String segment) throws IOException { - if (segment.equals("")) { - return; - } - writeString(" \n" + " " + Utils.cleanString(segment) + "\n" + " \n"); - writeSkeleton("%%%" + segId++ + "%%%"); - } - - private static int countQuotes(String line, char quote) { - int result = 0; - int index = line.indexOf(quote); - while (index != -1) { - result++; - if (index > 0 && line.charAt(index - 1) == '\\') { - result--; - } - if (index < line.length()) { - index++; - index = line.indexOf(quote, index); - } else { - index = -1; - } - } - return result; - } - - private static void writeString(String string) throws IOException { - output.write(string.getBytes(StandardCharsets.UTF_8)); - } - - private static void writeSkeleton(String string) throws IOException { - skeleton.write(string.getBytes(StandardCharsets.UTF_8)); - } -} +/******************************************************************************* + * Copyright (c) 2003-2019 Maxprograms. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/org/documents/epl-v10.html + * + * Contributors: + * Maxprograms - initial API and implementation + *******************************************************************************/ +package com.maxprograms.converters.javascript; + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Hashtable; +import java.util.Vector; +import java.lang.System.Logger.Level; +import java.lang.System.Logger; + +import com.maxprograms.converters.Constants; +import com.maxprograms.converters.Utils; + +public class Jscript2xliff { + + private static FileOutputStream output; + private static FileOutputStream skeleton; + private static String sourceLanguage; + private static int segId; + + private Jscript2xliff() { + // do not instantiate this class + // use run method instead + } + + public static Vector run(Hashtable params) { + Vector result = new Vector<>(); + + String inputFile = params.get("source"); + String xliffFile = params.get("xliff"); + String skeletonFile = params.get("skeleton"); + sourceLanguage = params.get("srcLang"); + String targetLanguage = params.get("tgtLang"); + String encoding = params.get("srcEncoding"); + String tgtLang = ""; + if (targetLanguage != null) { + tgtLang = "\" target-language=\"" + targetLanguage; + } + + try { + try (FileInputStream stream = new FileInputStream(inputFile)) { + try (InputStreamReader input = new InputStreamReader(stream, encoding)) { + BufferedReader buffer = new BufferedReader(input); + output = new FileOutputStream(xliffFile); + + writeString("\n"); + writeString("\n"); + writeString("\n"); + writeString("\n"); + writeString("
\n"); + writeString(" \n"); + writeString(" \n"); + writeString(" \n"); + writeString("
\n"); + writeString("\n"); + + skeleton = new FileOutputStream(skeletonFile); + + String line; + String comment = ""; + while ((line = buffer.readLine()) != null) { + line = line + "\n"; + comment = findComment(line); + if (!comment.equals("")) { + line = line.substring(0, line.indexOf(comment)); + } + + // TODO: check for /* block comments */ + + if (line.indexOf('\"') == -1 && line.indexOf('\'') == -1) { + // no text in this line + writeSkeleton(line + comment); + } else { + // check for strings to extract + int number = countQuotes(line, '\"') + countQuotes(line, '\''); + if (number > 0 && number % 2 == 0) { + // all strings closed in the same line + extractStrings(line); + writeSkeleton(comment); + } else { + // check if the line ends with "/" + if (line.trim().endsWith("/") && !line.trim().endsWith("//")) { + String nextLine = buffer.readLine(); + if (nextLine == null) { + result.add(Constants.ERROR); + result.add("Unexpected end of file."); + return result; + } + comment = findComment(nextLine); + if (!comment.equals("")) { + nextLine = nextLine.substring(0, nextLine.indexOf(comment)); + } + line = line + nextLine; + continue; + } + result.add(Constants.ERROR); + result.add("Found a string that is not properly closed."); + return result; + } + } + } + + skeleton.close(); + + writeString("\n"); + writeString("
\n"); + writeString("
"); + } + } + output.close(); + result.add(Constants.SUCCESS); + } catch (IOException e) { + Logger logger = System.getLogger(Jscript2xliff.class.getName()); + logger.log(Level.ERROR, "Error converting JavaScript file.", e); + result.add(Constants.ERROR); + result.add(e.getMessage()); + } + return result; + } + + private static String findComment(String line) { + boolean inString = false; + for (int i = 0; i < line.length(); i++) { + char c = line.charAt(i); + if (c == '\"' || c == '\'') { + if (i > 0) { + // not at start of line + if (inString) { + if (line.charAt(i - 1) == '\\') { + // escaped quote, ignore + } else { + // close the string + inString = false; + } + } else { + // string starts here + inString = true; + } + } else { + // start of line + inString = true; + } + } + if (!inString && c == '/' && i + 1 < line.length() && line.charAt(i + 1) == '/') { + // it's a comment! + return line.substring(i); + } + } + return ""; + } + + private static void extractStrings(String line) throws IOException { + while (line.length() > 0) { + line = checkForQuote(line, '\"'); + line = checkForQuote(line, '\''); + if (line.indexOf('\"') == -1 && line.indexOf('\'') == -1) { + // no more quoted sections in the string + writeSkeleton(line); + line = ""; + } + } // line length > 0 ? + } + + private static String checkForQuote(String line, char c) throws IOException { + boolean isString = true; + int index = line.indexOf(c); + if (index > 0 && line.charAt(index - 1) == '\\') { + isString = false; + } + if (isString) { + String start = line.substring(0, index + 1); + writeSkeleton(start); + line = line.substring(index + 1); + StringBuilder buff = new StringBuilder(); + for (int i = 0; i < line.length(); i++) { + if (line.charAt(i) == c) { + boolean endsString = true; + if (i > 0 && line.charAt(i - 1) == '\\') { + endsString = false; + } + if (endsString) { + writeSegment(buff.toString()); + line = line.substring(buff.toString().length() + 1); + writeSkeleton("" + c); + break; + } + } + buff.append(line.charAt(i)); + } + } + return line; + } + + private static void writeSegment(String segment) throws IOException { + if (segment.equals("")) { + return; + } + writeString(" \n" + " " + Utils.cleanString(segment) + "\n" + " \n"); + writeSkeleton("%%%" + segId++ + "%%%"); + } + + private static int countQuotes(String line, char quote) { + int result = 0; + int index = line.indexOf(quote); + while (index != -1) { + result++; + if (index > 0 && line.charAt(index - 1) == '\\') { + result--; + } + if (index < line.length()) { + index++; + index = line.indexOf(quote, index); + } else { + index = -1; + } + } + return result; + } + + private static void writeString(String string) throws IOException { + output.write(string.getBytes(StandardCharsets.UTF_8)); + } + + private static void writeSkeleton(String string) throws IOException { + skeleton.write(string.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/src/com/maxprograms/converters/javascript/Xliff2jscript.java b/src/com/maxprograms/converters/javascript/Xliff2jscript.java index ae2cb4cd..f945167d 100644 --- a/src/com/maxprograms/converters/javascript/Xliff2jscript.java +++ b/src/com/maxprograms/converters/javascript/Xliff2jscript.java @@ -1,144 +1,145 @@ -/******************************************************************************* - * Copyright (c) 2003-2019 Maxprograms. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 1.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/org/documents/epl-v10.html - * - * Contributors: - * Maxprograms - initial API and implementation - *******************************************************************************/ -package com.maxprograms.converters.javascript; - -import java.io.BufferedReader; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.List; -import java.util.Vector; - -import javax.xml.parsers.ParserConfigurationException; - -import org.xml.sax.SAXException; - -import com.maxprograms.converters.UnexistentSegmentException; -import com.maxprograms.xml.Catalog; -import com.maxprograms.xml.Document; -import com.maxprograms.xml.Element; -import com.maxprograms.xml.SAXBuilder; - -public class Xliff2jscript { - - private static String xliffFile; - private static Hashtable segments; - private static FileOutputStream output; - private static String catalog; - private static String encoding; - - private Xliff2jscript() { - // do not instantiate this class - // use run method instead - } - - public static Vector run(Hashtable params) { - - Vector result = new Vector<>(); - - String sklFile = params.get("skeleton"); - xliffFile = params.get("xliff"); - encoding = params.get("encoding"); - catalog = params.get("catalog"); - - try { - String outputFile = params.get("backfile"); - output = new FileOutputStream(outputFile); - loadSegments(); - try (InputStreamReader input = new InputStreamReader(new FileInputStream(sklFile), - StandardCharsets.UTF_8)) { - BufferedReader buffer = new BufferedReader(input); - String line; - while ((line = buffer.readLine()) != null) { - line = line + "\n"; - - if (line.indexOf("%%%") != -1) { - // - // contains translatable text - // - int index = line.indexOf("%%%"); - while (index != -1) { - String start = line.substring(0, index); - writeString(start); - line = line.substring(index + 3); - String code = line.substring(0, line.indexOf("%%%")); - line = line.substring(line.indexOf("%%%") + 3); - Element segment = segments.get(code); - if (segment != null) { - - if (segment.getAttributeValue("approved", "no").equalsIgnoreCase("Yes")) { - Element target = segment.getChild("target"); - if (target != null) { - writeString(target.getText()); - } else { - throw new UnexistentSegmentException("Missing target for segment " + code); - } - } else { - // process source - Element source = segment.getChild("source"); - writeString(source.getText()); - } - } else { - throw new UnexistentSegmentException("Missing segment " + code); - } - - index = line.indexOf("%%%"); - if (index == -1) { - writeString(line); - } - } // end while - } else { - // - // non translatable portion - // - writeString(line); - } - } - } - output.close(); - result.add("0"); - } catch (IOException | SAXException | ParserConfigurationException | UnexistentSegmentException e) { - result.add("1"); - result.add(e.getMessage()); - } - - return result; - } - - private static void loadSegments() throws SAXException, IOException, ParserConfigurationException { - - SAXBuilder builder = new SAXBuilder(); - builder.setEntityResolver(new Catalog(catalog)); - - Document doc = builder.build(xliffFile); - Element root = doc.getRootElement(); - Element body = root.getChild("file").getChild("body"); - List units = body.getChildren("trans-unit"); - Iterator i = units.iterator(); - - segments = new Hashtable<>(); - - while (i.hasNext()) { - Element unit = i.next(); - segments.put(unit.getAttributeValue("id"), unit); - } - } - - private static void writeString(String string) throws IOException { - output.write(string.getBytes(encoding)); - } - -} +/******************************************************************************* + * Copyright (c) 2003-2019 Maxprograms. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/org/documents/epl-v10.html + * + * Contributors: + * Maxprograms - initial API and implementation + *******************************************************************************/ +package com.maxprograms.converters.javascript; + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Vector; + +import javax.xml.parsers.ParserConfigurationException; + +import org.xml.sax.SAXException; + +import com.maxprograms.converters.Constants; +import com.maxprograms.converters.UnexistentSegmentException; +import com.maxprograms.xml.Catalog; +import com.maxprograms.xml.Document; +import com.maxprograms.xml.Element; +import com.maxprograms.xml.SAXBuilder; + +public class Xliff2jscript { + + private static String xliffFile; + private static Hashtable segments; + private static FileOutputStream output; + private static String catalog; + private static String encoding; + + private Xliff2jscript() { + // do not instantiate this class + // use run method instead + } + + public static Vector run(Hashtable params) { + + Vector result = new Vector<>(); + + String sklFile = params.get("skeleton"); + xliffFile = params.get("xliff"); + encoding = params.get("encoding"); + catalog = params.get("catalog"); + + try { + String outputFile = params.get("backfile"); + output = new FileOutputStream(outputFile); + loadSegments(); + try (InputStreamReader input = new InputStreamReader(new FileInputStream(sklFile), + StandardCharsets.UTF_8)) { + BufferedReader buffer = new BufferedReader(input); + String line; + while ((line = buffer.readLine()) != null) { + line = line + "\n"; + + if (line.indexOf("%%%") != -1) { + // + // contains translatable text + // + int index = line.indexOf("%%%"); + while (index != -1) { + String start = line.substring(0, index); + writeString(start); + line = line.substring(index + 3); + String code = line.substring(0, line.indexOf("%%%")); + line = line.substring(line.indexOf("%%%") + 3); + Element segment = segments.get(code); + if (segment != null) { + + if (segment.getAttributeValue("approved", "no").equalsIgnoreCase("Yes")) { + Element target = segment.getChild("target"); + if (target != null) { + writeString(target.getText()); + } else { + throw new UnexistentSegmentException("Missing target for segment " + code); + } + } else { + // process source + Element source = segment.getChild("source"); + writeString(source.getText()); + } + } else { + throw new UnexistentSegmentException("Missing segment " + code); + } + + index = line.indexOf("%%%"); + if (index == -1) { + writeString(line); + } + } // end while + } else { + // + // non translatable portion + // + writeString(line); + } + } + } + output.close(); + result.add(Constants.SUCCESS); + } catch (IOException | SAXException | ParserConfigurationException | UnexistentSegmentException e) { + result.add(Constants.ERROR); + result.add(e.getMessage()); + } + + return result; + } + + private static void loadSegments() throws SAXException, IOException, ParserConfigurationException { + + SAXBuilder builder = new SAXBuilder(); + builder.setEntityResolver(new Catalog(catalog)); + + Document doc = builder.build(xliffFile); + Element root = doc.getRootElement(); + Element body = root.getChild("file").getChild("body"); + List units = body.getChildren("trans-unit"); + Iterator i = units.iterator(); + + segments = new Hashtable<>(); + + while (i.hasNext()) { + Element unit = i.next(); + segments.put(unit.getAttributeValue("id"), unit); + } + } + + private static void writeString(String string) throws IOException { + output.write(string.getBytes(encoding)); + } + +} diff --git a/src/com/maxprograms/converters/mif/Mif2Xliff.java b/src/com/maxprograms/converters/mif/Mif2Xliff.java index c8727e53..a7a72889 100644 --- a/src/com/maxprograms/converters/mif/Mif2Xliff.java +++ b/src/com/maxprograms/converters/mif/Mif2Xliff.java @@ -1,382 +1,383 @@ -/******************************************************************************* - * Copyright (c) 2003-2019 Maxprograms. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 1.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/org/documents/epl-v10.html - * - * Contributors: - * Maxprograms - initial API and implementation - *******************************************************************************/ -package com.maxprograms.converters.mif; - -import java.io.BufferedReader; -import java.io.FileOutputStream; -import java.io.FileReader; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.List; -import java.util.Stack; -import java.util.Vector; -import java.lang.System.Logger.Level; -import java.lang.System.Logger; - -import javax.xml.parsers.ParserConfigurationException; - -import org.xml.sax.SAXException; - -import com.maxprograms.converters.Utils; -import com.maxprograms.xml.Document; -import com.maxprograms.xml.Element; -import com.maxprograms.xml.SAXBuilder; - -public class Mif2Xliff { - - private static FileOutputStream output; - private static FileOutputStream skeleton; - private static String sourceLanguage; - private static ArrayList translatable; - private static String segment; - private static int segId; - private static Hashtable charmap; - - private Mif2Xliff() { - // do not instantiate this class - // use run method instead - } - - public static Vector run(Hashtable params) { - - Vector result = new Vector<>(); - - String inputFile = params.get("source"); - String xliffFile = params.get("xliff"); - String skeletonFile = params.get("skeleton"); - sourceLanguage = params.get("srcLang"); - String targetLanguage = params.get("tgtLang"); - String encoding = params.get("srcEncoding"); - String tgtLang = ""; - if (targetLanguage != null) { - tgtLang = "\" target-language=\"" + targetLanguage; - } - - fillTranslatable(); - - boolean inPara = false; - segment = ""; - segId = 1; - int tagId = 1; - - try { - loadCharMap(); - try (FileReader input = new FileReader(inputFile)) { - BufferedReader buffer = new BufferedReader(input); - - output = new FileOutputStream(xliffFile); - writeString("\n"); - writeString("\n"); - writeString("\n"); - writeString("\n"); - writeString("
\n"); - writeString(" \n"); - writeString(" \n"); - writeString(" \n"); - writeString("
\n"); - writeString("\n"); - - skeleton = new FileOutputStream(skeletonFile); - - String type = null; - String content = null; - int starts; - int ends; - Stack typesList = new Stack<>(); - - String line; - - while ((line = buffer.readLine()) != null) { - if (line.length() == 0) { - line = buffer.readLine(); - continue; - } - char first = line.charAt(0); - int count = 0; - while (Character.isSpaceChar(first) && count < line.length()) { - first = line.charAt(count++); - } - if (first == '&' || first == '=') { - writeSkeleton(line + "\n"); - line = buffer.readLine(); - continue; - } - starts = line.indexOf('<'); - ends = line.indexOf('>'); - - if (starts != -1 && first == '<') { // element starts here - String trimmed = line.substring(starts + 1); - int stops = trimmed.indexOf(' '); - if (ends == -1) { - // no clossing '>' - // compound element starts here - type = trimmed.substring(0, trimmed.length() - 1).toLowerCase(); - typesList.push(type.toLowerCase()); - if (type.equals("para")) { - inPara = true; - } - } else { - // the full element is defined in one line - // get the content - if (stops != -1) { - type = trimmed.substring(0, stops).toLowerCase(); - } else { - // we are not in an element! - // ignore and let crash - } - } - if (inPara) { - if (!translatable.contains(type)) { - if (!segment.equals("")) { - if (segment.endsWith("
")) { - segment = segment.substring(0, segment.length() - 5); - segment += "\n" + cleanTag(line) + "
"; - } else { - segment += "" + cleanTag(line) + ""; - } - } else { - writeSkeleton(line + "\n"); - } - } else { - content = trimmed.substring(stops + 2, trimmed.lastIndexOf("'>")); - // remove comments at the end of the line - content = removeComments(content); - // check for ilegal characters - content = cleanString(replaceChars(content)); - if (segment.equals("")) { - writeSkeleton("%%%" + segId + "%%%\n"); - } - segment += content; - } - } else { - writeSkeleton(line + "\n"); - } - } - - if (ends != -1 && starts == -1) { - // close previous compound element - if (!typesList.isEmpty()) { - type = typesList.pop(); - } - if (inPara && !segment.equals("")) { - if (segment.endsWith("
")) { - segment = segment.substring(0, segment.length() - 5); - segment += "\n" + cleanTag(line) + "
"; - } else { - segment += "" + cleanTag(line) + ""; - } - } else { - writeSkeleton(line + "\n"); - } - if (type != null && type.equals("para")) { - if (!segment.equals("")) { - writeSegment(); - } - inPara = false; - segment = ""; - tagId = 1; - } - } - - if (ends == -1 && starts == -1) { - // write comment to skeleton - writeSkeleton(line + "\n"); - } - } - - skeleton.close(); - - writeString("\n"); - writeString("\n"); - writeString(""); - } - output.close(); - result.add("0"); - } catch (IOException | SAXException | ParserConfigurationException e) { - Logger logger = System.getLogger(Mif2Xliff.class.getName()); - logger.log(Level.ERROR, "Error converting MIF file", e); - result.add("1"); - result.add(e.getMessage()); - } - - return result; - } - - private static String replaceChars(String string) { - string = replaceAll(string, "\\t", "\u0009"); - string = replaceAll(string, "\\q", "\'"); - string = replaceAll(string, "\\Q", "\""); - string = replaceAll(string, "\\>", ">"); - string = replaceAll(string, "\\\\", "\\"); - return string; - } - - private static String replaceAll(String string, String token, String newText) { - int index = string.indexOf(token); - while (index != -1) { - String before = string.substring(0, index); - String after = string.substring(index + token.length()); - string = before + newText + after; - index = string.indexOf(token, index + newText.length()); - } - return string; - } - - /** - * This method cleans the text that will be stored inside elements in the - * XLIFF file * - */ - private static String cleanTag(String line) { - String s = line.replaceAll("&", "&"); - s = s.replaceAll("<", "<"); - s = s.replaceAll(">", ">"); - return s; - } - - /** - * This method saves each segment into the XLIFF file - * - * If segment text ends in a element, it is removed and added to the - * skeleton file instead. - */ - private static void writeSegment() throws IOException { - if (segment.equals("")) { - return; - } - if (segment.endsWith("")) { - String ph = segment.substring(segment.lastIndexOf("') + 1, ph.length() - 5); - writeSkeleton(restoreTags(ph) + "\n"); - } - writeString(" \n" + " " + segment + "\n" + " \n"); - } - - /** - * This method restores '&', ' <' and '>' characters to the text that will be - * saved in the skeleton file. Those characters were converted to entities when - * they were tentatively added to the segment. - */ - private static String restoreTags(String ph) { - int index = ph.indexOf(">"); - while (index != -1) { - ph = ph.substring(0, index) + ">" + ph.substring(index + 4); - index = ph.indexOf(">", index); - } - index = ph.indexOf("<"); - while (index != -1) { - ph = ph.substring(0, index) + "<" + ph.substring(index + 4); - index = ph.indexOf("<", index); - } - index = ph.indexOf("&"); - while (index != -1) { - ph = ph.substring(0, index) + "&" + ph.substring(index + 5); - index = ph.indexOf("&", index); - } - return ph; - } - - private static void writeString(String string) throws IOException { - output.write(string.getBytes(StandardCharsets.UTF_8)); - } - - private static void writeSkeleton(String string) throws IOException { - skeleton.write(string.getBytes(StandardCharsets.UTF_8)); - } - - private static String cleanString(String s) { - int control = s.indexOf("\\x"); - while (control != -1) { - String code = s.substring(control + 2, s.indexOf(' ', control)); - - String character = "" + getCharValue(Integer.valueOf(code, 16).intValue()); - if (!character.equals("")) { - s = s.substring(0, control) + character + s.substring(1 + s.indexOf(' ', control)); - } - control++; - control = s.indexOf("\\x", control); - } - - return cleanTag(s); - } - - private static void fillTranslatable() { - translatable = new ArrayList<>(); - translatable.add("string"); - } - - private static String removeComments(String string) { - int ends = string.lastIndexOf('>'); - if (ends != -1 && ends < string.lastIndexOf('#')) { - string = string.substring(0, string.lastIndexOf('>')); - } - return string; - } - - private static char getCharValue(int value) { - switch (value) { - case 0x04: - return '\u0004'; - case 0x05: - return '\u0005'; - case 0x08: - return '\u0008'; - case 0x09: - return '\u0009'; - case 0x0a: - return '\u0010'; - case 0x10: - return '\u0016'; - case 0x11: - return '\u0017'; - case 0x12: - return '\u0018'; - case 0x13: - return '\u0019'; - case 0x14: - return '\u0020'; - case 0x15: - return '\u0021'; - } - if (value > 0x7f) { - String key = "\\x" + Integer.toHexString(value); - if (charmap.containsKey(key)) { - String result = charmap.get(key); - if (result.length() > 0) { - return result.charAt(0); - } - } - } - return (char) value; - } - - private static void loadCharMap() throws SAXException, IOException, ParserConfigurationException { - SAXBuilder cbuilder = new SAXBuilder(); - Document cdoc = cbuilder.build("xmlfilter/init_mif.xml"); - charmap = new Hashtable<>(); - Element croot = cdoc.getRootElement(); - List codes = croot.getChildren("char"); - Iterator it = codes.iterator(); - while (it.hasNext()) { - Element e = it.next(); - charmap.put(e.getAttributeValue("code"), e.getText()); - } - } - -} +/******************************************************************************* + * Copyright (c) 2003-2019 Maxprograms. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/org/documents/epl-v10.html + * + * Contributors: + * Maxprograms - initial API and implementation + *******************************************************************************/ +package com.maxprograms.converters.mif; + +import java.io.BufferedReader; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Stack; +import java.util.Vector; +import java.lang.System.Logger.Level; +import java.lang.System.Logger; + +import javax.xml.parsers.ParserConfigurationException; + +import org.xml.sax.SAXException; + +import com.maxprograms.converters.Constants; +import com.maxprograms.converters.Utils; +import com.maxprograms.xml.Document; +import com.maxprograms.xml.Element; +import com.maxprograms.xml.SAXBuilder; + +public class Mif2Xliff { + + private static FileOutputStream output; + private static FileOutputStream skeleton; + private static String sourceLanguage; + private static ArrayList translatable; + private static String segment; + private static int segId; + private static Hashtable charmap; + + private Mif2Xliff() { + // do not instantiate this class + // use run method instead + } + + public static Vector run(Hashtable params) { + + Vector result = new Vector<>(); + + String inputFile = params.get("source"); + String xliffFile = params.get("xliff"); + String skeletonFile = params.get("skeleton"); + sourceLanguage = params.get("srcLang"); + String targetLanguage = params.get("tgtLang"); + String encoding = params.get("srcEncoding"); + String tgtLang = ""; + if (targetLanguage != null) { + tgtLang = "\" target-language=\"" + targetLanguage; + } + + fillTranslatable(); + + boolean inPara = false; + segment = ""; + segId = 1; + int tagId = 1; + + try { + loadCharMap(); + try (FileReader input = new FileReader(inputFile)) { + BufferedReader buffer = new BufferedReader(input); + + output = new FileOutputStream(xliffFile); + writeString("\n"); + writeString("\n"); + writeString("\n"); + writeString("\n"); + writeString("
\n"); + writeString(" \n"); + writeString(" \n"); + writeString(" \n"); + writeString("
\n"); + writeString("\n"); + + skeleton = new FileOutputStream(skeletonFile); + + String type = null; + String content = null; + int starts; + int ends; + Stack typesList = new Stack<>(); + + String line; + + while ((line = buffer.readLine()) != null) { + if (line.length() == 0) { + line = buffer.readLine(); + continue; + } + char first = line.charAt(0); + int count = 0; + while (Character.isSpaceChar(first) && count < line.length()) { + first = line.charAt(count++); + } + if (first == '&' || first == '=') { + writeSkeleton(line + "\n"); + line = buffer.readLine(); + continue; + } + starts = line.indexOf('<'); + ends = line.indexOf('>'); + + if (starts != -1 && first == '<') { // element starts here + String trimmed = line.substring(starts + 1); + int stops = trimmed.indexOf(' '); + if (ends == -1) { + // no clossing '>' + // compound element starts here + type = trimmed.substring(0, trimmed.length() - 1).toLowerCase(); + typesList.push(type.toLowerCase()); + if (type.equals("para")) { + inPara = true; + } + } else { + // the full element is defined in one line + // get the content + if (stops != -1) { + type = trimmed.substring(0, stops).toLowerCase(); + } else { + // we are not in an element! + // ignore and let crash + } + } + if (inPara) { + if (!translatable.contains(type)) { + if (!segment.equals("")) { + if (segment.endsWith("
")) { + segment = segment.substring(0, segment.length() - 5); + segment += "\n" + cleanTag(line) + "
"; + } else { + segment += "" + cleanTag(line) + ""; + } + } else { + writeSkeleton(line + "\n"); + } + } else { + content = trimmed.substring(stops + 2, trimmed.lastIndexOf("'>")); + // remove comments at the end of the line + content = removeComments(content); + // check for ilegal characters + content = cleanString(replaceChars(content)); + if (segment.equals("")) { + writeSkeleton("%%%" + segId + "%%%\n"); + } + segment += content; + } + } else { + writeSkeleton(line + "\n"); + } + } + + if (ends != -1 && starts == -1) { + // close previous compound element + if (!typesList.isEmpty()) { + type = typesList.pop(); + } + if (inPara && !segment.equals("")) { + if (segment.endsWith("")) { + segment = segment.substring(0, segment.length() - 5); + segment += "\n" + cleanTag(line) + ""; + } else { + segment += "" + cleanTag(line) + ""; + } + } else { + writeSkeleton(line + "\n"); + } + if (type != null && type.equals("para")) { + if (!segment.equals("")) { + writeSegment(); + } + inPara = false; + segment = ""; + tagId = 1; + } + } + + if (ends == -1 && starts == -1) { + // write comment to skeleton + writeSkeleton(line + "\n"); + } + } + + skeleton.close(); + + writeString("\n"); + writeString("\n"); + writeString(""); + } + output.close(); + result.add(Constants.SUCCESS); + } catch (IOException | SAXException | ParserConfigurationException e) { + Logger logger = System.getLogger(Mif2Xliff.class.getName()); + logger.log(Level.ERROR, "Error converting MIF file", e); + result.add(Constants.ERROR); + result.add(e.getMessage()); + } + + return result; + } + + private static String replaceChars(String string) { + string = replaceAll(string, "\\t", "\u0009"); + string = replaceAll(string, "\\q", "\'"); + string = replaceAll(string, "\\Q", "\""); + string = replaceAll(string, "\\>", ">"); + string = replaceAll(string, "\\\\", "\\"); + return string; + } + + private static String replaceAll(String string, String token, String newText) { + int index = string.indexOf(token); + while (index != -1) { + String before = string.substring(0, index); + String after = string.substring(index + token.length()); + string = before + newText + after; + index = string.indexOf(token, index + newText.length()); + } + return string; + } + + /** + * This method cleans the text that will be stored inside elements in the + * XLIFF file * + */ + private static String cleanTag(String line) { + String s = line.replaceAll("&", "&"); + s = s.replaceAll("<", "<"); + s = s.replaceAll(">", ">"); + return s; + } + + /** + * This method saves each segment into the XLIFF file + * + * If segment text ends in a element, it is removed and added to the + * skeleton file instead. + */ + private static void writeSegment() throws IOException { + if (segment.equals("")) { + return; + } + if (segment.endsWith("")) { + String ph = segment.substring(segment.lastIndexOf("') + 1, ph.length() - 5); + writeSkeleton(restoreTags(ph) + "\n"); + } + writeString(" \n" + " " + segment + "\n" + " \n"); + } + + /** + * This method restores '&', ' <' and '>' characters to the text that will be + * saved in the skeleton file. Those characters were converted to entities when + * they were tentatively added to the segment. + */ + private static String restoreTags(String ph) { + int index = ph.indexOf(">"); + while (index != -1) { + ph = ph.substring(0, index) + ">" + ph.substring(index + 4); + index = ph.indexOf(">", index); + } + index = ph.indexOf("<"); + while (index != -1) { + ph = ph.substring(0, index) + "<" + ph.substring(index + 4); + index = ph.indexOf("<", index); + } + index = ph.indexOf("&"); + while (index != -1) { + ph = ph.substring(0, index) + "&" + ph.substring(index + 5); + index = ph.indexOf("&", index); + } + return ph; + } + + private static void writeString(String string) throws IOException { + output.write(string.getBytes(StandardCharsets.UTF_8)); + } + + private static void writeSkeleton(String string) throws IOException { + skeleton.write(string.getBytes(StandardCharsets.UTF_8)); + } + + private static String cleanString(String s) { + int control = s.indexOf("\\x"); + while (control != -1) { + String code = s.substring(control + 2, s.indexOf(' ', control)); + + String character = "" + getCharValue(Integer.valueOf(code, 16).intValue()); + if (!character.equals("")) { + s = s.substring(0, control) + character + s.substring(1 + s.indexOf(' ', control)); + } + control++; + control = s.indexOf("\\x", control); + } + + return cleanTag(s); + } + + private static void fillTranslatable() { + translatable = new ArrayList<>(); + translatable.add("string"); + } + + private static String removeComments(String string) { + int ends = string.lastIndexOf('>'); + if (ends != -1 && ends < string.lastIndexOf('#')) { + string = string.substring(0, string.lastIndexOf('>')); + } + return string; + } + + private static char getCharValue(int value) { + switch (value) { + case 0x04: + return '\u0004'; + case 0x05: + return '\u0005'; + case 0x08: + return '\u0008'; + case 0x09: + return '\u0009'; + case 0x0a: + return '\u0010'; + case 0x10: + return '\u0016'; + case 0x11: + return '\u0017'; + case 0x12: + return '\u0018'; + case 0x13: + return '\u0019'; + case 0x14: + return '\u0020'; + case 0x15: + return '\u0021'; + } + if (value > 0x7f) { + String key = "\\x" + Integer.toHexString(value); + if (charmap.containsKey(key)) { + String result = charmap.get(key); + if (result.length() > 0) { + return result.charAt(0); + } + } + } + return (char) value; + } + + private static void loadCharMap() throws SAXException, IOException, ParserConfigurationException { + SAXBuilder cbuilder = new SAXBuilder(); + Document cdoc = cbuilder.build("xmlfilter/init_mif.xml"); + charmap = new Hashtable<>(); + Element croot = cdoc.getRootElement(); + List codes = croot.getChildren("char"); + Iterator it = codes.iterator(); + while (it.hasNext()) { + Element e = it.next(); + charmap.put(e.getAttributeValue("code"), e.getText()); + } + } + +} diff --git a/src/com/maxprograms/converters/mif/Xliff2Mif.java b/src/com/maxprograms/converters/mif/Xliff2Mif.java index 7bb1b938..db1f8b68 100644 --- a/src/com/maxprograms/converters/mif/Xliff2Mif.java +++ b/src/com/maxprograms/converters/mif/Xliff2Mif.java @@ -1,215 +1,216 @@ -/******************************************************************************* - * Copyright (c) 2003-2019 Maxprograms. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 1.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/org/documents/epl-v10.html - * - * Contributors: - * Maxprograms - initial API and implementation - *******************************************************************************/ -package com.maxprograms.converters.mif; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.List; -import java.util.Vector; -import java.lang.System.Logger.Level; -import java.lang.System.Logger; - -import javax.xml.parsers.ParserConfigurationException; - -import org.xml.sax.SAXException; - -import com.maxprograms.converters.UnexistentSegmentException; -import com.maxprograms.xml.Catalog; -import com.maxprograms.xml.Document; -import com.maxprograms.xml.Element; -import com.maxprograms.xml.SAXBuilder; -import com.maxprograms.xml.TextNode; -import com.maxprograms.xml.XMLNode; - -public class Xliff2Mif { - - private static String xliffFile; - private static Hashtable segments; - private static FileOutputStream output; - private static Hashtable charmap; - private static String catalog; - private static boolean useUnicode; - - private Xliff2Mif() { - // do not instantiate this class - // use run method instead - } - - public static Vector run(Hashtable params) { - Vector result = new Vector<>(); - - String sklFile = params.get("skeleton"); - xliffFile = params.get("xliff"); - catalog = params.get("catalog"); - - try { - File f = new File(params.get("backfile")); - File p = f.getParentFile(); - if (p == null) { - p = new File(System.getProperty("user.dir")); - } - if (!p.exists()) { - p.mkdirs(); - } - if (!f.exists()) { - Files.createFile(Paths.get(f.toURI())); - } - output = new FileOutputStream(f); - loadSegments(); - loadCharMap(); - - try (InputStreamReader input = new InputStreamReader(new FileInputStream(sklFile), - StandardCharsets.UTF_8)) { - BufferedReader buffer = new BufferedReader(input); - String line = buffer.readLine(); - if (line.indexOf("") != -1) { - useUnicode = true; - } - while (line != null) { - if (line.startsWith("%%%")) { - // - // translatable text - // - String code = line.substring(3, line.length() - 3); - Element segment = segments.get(code); - if (segment != null) { - if (segment.getAttributeValue("approved", "no").equals("yes")) { - Element target = segment.getChild("target"); - if (target != null) { - process(target); - } else { - throw new UnexistentSegmentException("Missing target in segment " + code); - } - } else { - // process source - Element source = segment.getChild("source"); - process(source); - } - } else { - throw new UnexistentSegmentException("Missing segment " + code); - } - } else { - // - // non translatable portion - // - writeString(line + "\n"); - } - line = buffer.readLine(); - } - } - output.close(); - result.add("0"); - } catch (IOException | SAXException | ParserConfigurationException | UnexistentSegmentException e) { - Logger logger = System.getLogger(Xliff2Mif.class.getName()); - logger.log(Level.ERROR, "Error mering MIF file", e); - result.add("1"); - result.add(e.getMessage()); - } - return result; - } - - private static void loadCharMap() throws SAXException, IOException, ParserConfigurationException { - SAXBuilder cbuilder = new SAXBuilder(); - Document cdoc = cbuilder.build("xmlfilter/init_mif.xml"); - charmap = new Hashtable<>(); - Element croot = cdoc.getRootElement(); - List codes = croot.getChildren("char"); - Iterator it = codes.iterator(); - while (it.hasNext()) { - Element e = it.next(); - charmap.put(e.getText(), e.getAttributeValue("code")); - } - } - - private static void process(Element e) throws IOException { - String result = ""; - List content = e.getContent(); - Iterator i = content.iterator(); - while (i.hasNext()) { - XMLNode n = i.next(); - if (n.getNodeType() == XMLNode.TEXT_NODE) { - result += " \n"; - } - if (n.getNodeType() == XMLNode.ELEMENT_NODE) { - Element el = (Element) n; - if (el.getName().equals("ph")) { - result += el.getText() + "\n"; - } - } - } - writeString(result); - } - - private static void loadSegments() throws SAXException, IOException, ParserConfigurationException { - SAXBuilder builder = new SAXBuilder(); - builder.setEntityResolver(new Catalog(catalog)); - - Document doc = builder.build(xliffFile); - Element root = doc.getRootElement(); - Element body = root.getChild("file").getChild("body"); - List units = body.getChildren("trans-unit"); - Iterator i = units.iterator(); - - segments = new Hashtable<>(); - - while (i.hasNext()) { - Element unit = i.next(); - segments.put(unit.getAttributeValue("id"), unit); - } - } - - private static String cleanString(String string) { - int length = string.length(); - StringBuilder buff = new StringBuilder(); - for (int i = 0; i < length; i++) { - buff.append(getCleanChar(string.charAt(i))); - } - return buff.toString(); - } - - private static String getCleanChar(char c) { - switch (c) { - case '\u0009': - return "\\t"; - case '\'': - return "\\q"; - case '`': - return "\\Q"; - case '\\': - return "\\\\"; - case '>': - return "\\>"; - } - if (useUnicode) { - return "" + c; - } - String s = "" + c; - if (c > '\u007f' && charmap.containsKey(s)) { - return charmap.get(s) + " "; - } - return "" + c; - } - - private static void writeString(String string) throws IOException { - output.write(string.getBytes()); - } - -} +/******************************************************************************* + * Copyright (c) 2003-2019 Maxprograms. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/org/documents/epl-v10.html + * + * Contributors: + * Maxprograms - initial API and implementation + *******************************************************************************/ +package com.maxprograms.converters.mif; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Vector; +import java.lang.System.Logger.Level; +import java.lang.System.Logger; + +import javax.xml.parsers.ParserConfigurationException; + +import org.xml.sax.SAXException; + +import com.maxprograms.converters.Constants; +import com.maxprograms.converters.UnexistentSegmentException; +import com.maxprograms.xml.Catalog; +import com.maxprograms.xml.Document; +import com.maxprograms.xml.Element; +import com.maxprograms.xml.SAXBuilder; +import com.maxprograms.xml.TextNode; +import com.maxprograms.xml.XMLNode; + +public class Xliff2Mif { + + private static String xliffFile; + private static Hashtable segments; + private static FileOutputStream output; + private static Hashtable charmap; + private static String catalog; + private static boolean useUnicode; + + private Xliff2Mif() { + // do not instantiate this class + // use run method instead + } + + public static Vector run(Hashtable params) { + Vector result = new Vector<>(); + + String sklFile = params.get("skeleton"); + xliffFile = params.get("xliff"); + catalog = params.get("catalog"); + + try { + File f = new File(params.get("backfile")); + File p = f.getParentFile(); + if (p == null) { + p = new File(System.getProperty("user.dir")); + } + if (!p.exists()) { + p.mkdirs(); + } + if (!f.exists()) { + Files.createFile(Paths.get(f.toURI())); + } + output = new FileOutputStream(f); + loadSegments(); + loadCharMap(); + + try (InputStreamReader input = new InputStreamReader(new FileInputStream(sklFile), + StandardCharsets.UTF_8)) { + BufferedReader buffer = new BufferedReader(input); + String line = buffer.readLine(); + if (line.indexOf("") != -1) { + useUnicode = true; + } + while (line != null) { + if (line.startsWith("%%%")) { + // + // translatable text + // + String code = line.substring(3, line.length() - 3); + Element segment = segments.get(code); + if (segment != null) { + if (segment.getAttributeValue("approved", "no").equals("yes")) { + Element target = segment.getChild("target"); + if (target != null) { + process(target); + } else { + throw new UnexistentSegmentException("Missing target in segment " + code); + } + } else { + // process source + Element source = segment.getChild("source"); + process(source); + } + } else { + throw new UnexistentSegmentException("Missing segment " + code); + } + } else { + // + // non translatable portion + // + writeString(line + "\n"); + } + line = buffer.readLine(); + } + } + output.close(); + result.add(Constants.SUCCESS); + } catch (IOException | SAXException | ParserConfigurationException | UnexistentSegmentException e) { + Logger logger = System.getLogger(Xliff2Mif.class.getName()); + logger.log(Level.ERROR, "Error mering MIF file", e); + result.add(Constants.ERROR); + result.add(e.getMessage()); + } + return result; + } + + private static void loadCharMap() throws SAXException, IOException, ParserConfigurationException { + SAXBuilder cbuilder = new SAXBuilder(); + Document cdoc = cbuilder.build("xmlfilter/init_mif.xml"); + charmap = new Hashtable<>(); + Element croot = cdoc.getRootElement(); + List codes = croot.getChildren("char"); + Iterator it = codes.iterator(); + while (it.hasNext()) { + Element e = it.next(); + charmap.put(e.getText(), e.getAttributeValue("code")); + } + } + + private static void process(Element e) throws IOException { + String result = ""; + List content = e.getContent(); + Iterator i = content.iterator(); + while (i.hasNext()) { + XMLNode n = i.next(); + if (n.getNodeType() == XMLNode.TEXT_NODE) { + result += " \n"; + } + if (n.getNodeType() == XMLNode.ELEMENT_NODE) { + Element el = (Element) n; + if (el.getName().equals("ph")) { + result += el.getText() + "\n"; + } + } + } + writeString(result); + } + + private static void loadSegments() throws SAXException, IOException, ParserConfigurationException { + SAXBuilder builder = new SAXBuilder(); + builder.setEntityResolver(new Catalog(catalog)); + + Document doc = builder.build(xliffFile); + Element root = doc.getRootElement(); + Element body = root.getChild("file").getChild("body"); + List units = body.getChildren("trans-unit"); + Iterator i = units.iterator(); + + segments = new Hashtable<>(); + + while (i.hasNext()) { + Element unit = i.next(); + segments.put(unit.getAttributeValue("id"), unit); + } + } + + private static String cleanString(String string) { + int length = string.length(); + StringBuilder buff = new StringBuilder(); + for (int i = 0; i < length; i++) { + buff.append(getCleanChar(string.charAt(i))); + } + return buff.toString(); + } + + private static String getCleanChar(char c) { + switch (c) { + case '\u0009': + return "\\t"; + case '\'': + return "\\q"; + case '`': + return "\\Q"; + case '\\': + return "\\\\"; + case '>': + return "\\>"; + } + if (useUnicode) { + return "" + c; + } + String s = "" + c; + if (c > '\u007f' && charmap.containsKey(s)) { + return charmap.get(s) + " "; + } + return "" + c; + } + + private static void writeString(String string) throws IOException { + output.write(string.getBytes()); + } + +} diff --git a/src/com/maxprograms/converters/msoffice/MSOffice2Xliff.java b/src/com/maxprograms/converters/msoffice/MSOffice2Xliff.java index 0bf68114..7a5f21ce 100644 --- a/src/com/maxprograms/converters/msoffice/MSOffice2Xliff.java +++ b/src/com/maxprograms/converters/msoffice/MSOffice2Xliff.java @@ -28,6 +28,7 @@ import org.xml.sax.SAXException; +import com.maxprograms.converters.Constants; import com.maxprograms.converters.Utils; import com.maxprograms.segmenter.Segmenter; import com.maxprograms.xml.Attribute; @@ -96,11 +97,11 @@ public static Vector run(Hashtable params) { writeOut(" \n \n"); out.close(); skel.close(); - result.add("0"); + result.add(Constants.SUCCESS); } catch (IOException | SAXException | ParserConfigurationException e) { Logger logger = System.getLogger(MSOffice2Xliff.class.getName()); logger.log(Level.ERROR, "Error converting MS Office file", e); - result.add("1"); + result.add(Constants.ERROR); result.add(e.getMessage()); } diff --git a/src/com/maxprograms/converters/office/Office2Xliff.java b/src/com/maxprograms/converters/office/Office2Xliff.java index 5d2085f8..7c5b308d 100644 --- a/src/com/maxprograms/converters/office/Office2Xliff.java +++ b/src/com/maxprograms/converters/office/Office2Xliff.java @@ -1,395 +1,396 @@ -/******************************************************************************* - * Copyright (c) 2003-2019 Maxprograms. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 1.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/org/documents/epl-v10.html - * - * Contributors: - * Maxprograms - initial API and implementation - *******************************************************************************/ -package com.maxprograms.converters.office; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.List; -import java.util.TreeSet; -import java.util.Vector; -import java.lang.System.Logger.Level; -import java.lang.System.Logger; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; -import java.util.zip.ZipOutputStream; - -import javax.xml.parsers.ParserConfigurationException; - -import org.xml.sax.SAXException; - -import com.maxprograms.converters.FileFormats; -import com.maxprograms.converters.Utils; -import com.maxprograms.converters.msoffice.MSOffice2Xliff; -import com.maxprograms.converters.xml.Xml2Xliff; -import com.maxprograms.xml.Catalog; -import com.maxprograms.xml.Document; -import com.maxprograms.xml.Element; -import com.maxprograms.xml.Indenter; -import com.maxprograms.xml.PI; -import com.maxprograms.xml.SAXBuilder; -import com.maxprograms.xml.TextNode; -import com.maxprograms.xml.XMLNode; -import com.maxprograms.xml.XMLOutputter; - -public class Office2Xliff { - - private static Element mergedRoot; - private static ZipInputStream in; - private static ZipOutputStream out; - private static String inputFile; - private static String skeleton; - private static boolean isPPTX; - - private Office2Xliff() { - // do not instantiate this class - // use run method instead - } - - public static Vector run(Hashtable params) { - Vector result = new Vector<>(); - - inputFile = params.get("source"); - String xliff = params.get("xliff"); - skeleton = params.get("skeleton"); - String catalog = params.get("catalog"); - - try { - - Document merged = new Document(null, "xliff", null, null); - mergedRoot = merged.getRootElement(); - mergedRoot.setAttribute("version", "1.2"); - mergedRoot.setAttribute("xmlns", "urn:oasis:names:tc:xliff:document:1.2"); - mergedRoot.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); - mergedRoot.setAttribute("xsi:schemaLocation", - "urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd"); - mergedRoot.addContent("\n"); - - try { - out = new ZipOutputStream(new FileOutputStream(skeleton)); - in = new ZipInputStream(new FileInputStream(inputFile)); - } catch (IOException e) { - result.add("1"); - if (params.get("format").equals(FileFormats.OFF)) { - result.add("Selected file is not a Microsoft Office 2007 document."); - } else { - result.add("Wrong document type."); - } - return result; - } - ZipEntry entry = null; - while ((entry = in.getNextEntry()) != null) { - if (entry.getName().matches(".*\\.[xX][mM][lL]") && !(entry.getName().matches(".*slideMaster.*") - || entry.getName().matches(".*slideLayout.*") || entry.getName().matches(".*handoutMaster.*") - || entry.getName().matches(".*notesMaster.*"))) { - File f = new File(entry.getName()); - String name = f.getName(); - File tmp = File.createTempFile(name.substring(0, name.lastIndexOf('.')), ".xml"); - try (FileOutputStream output = new FileOutputStream(tmp.getAbsolutePath())) { - byte[] buf = new byte[1024]; - int len; - while ((len = in.read(buf)) > 0) { - output.write(buf, 0, len); - } - } - if (name.equals("content.xml")) { - cleanTags(tmp.getAbsolutePath(), catalog); - } - try { - Hashtable table = new Hashtable<>(); - table.put("source", tmp.getAbsolutePath()); - table.put("xliff", tmp.getAbsolutePath() + ".xlf"); - table.put("skeleton", tmp.getAbsolutePath() + ".skl"); - table.put("catalog", params.get("catalog")); - table.put("srcLang", params.get("srcLang")); - String tgtLang = params.get("tgtLang"); - if (tgtLang != null) { - table.put("tgtLang", tgtLang); - } - table.put("srcEncoding", params.get("srcEncoding")); - table.put("paragraph", params.get("paragraph")); - table.put("srxFile", params.get("srxFile")); - table.put("format", params.get("format")); - Vector res = null; - if (params.get("format").equals(FileFormats.OFF)) { - res = MSOffice2Xliff.run(table); - if (tmp.getName().indexOf("slide") != -1) { - isPPTX = true; - } - } else { - res = Xml2Xliff.run(table); - } - if ("0".equals(res.get(0))) { - if (countSegments(tmp.getAbsolutePath() + ".xlf") > 0) { - updateXliff(tmp.getAbsolutePath() + ".xlf", entry.getName()); - addFile(tmp.getAbsolutePath() + ".xlf"); - ZipEntry content = new ZipEntry(entry.getName() + ".skl"); - content.setMethod(ZipEntry.DEFLATED); - out.putNextEntry(content); - try (FileInputStream input = new FileInputStream(tmp.getAbsolutePath() + ".skl")) { - byte[] array = new byte[1024]; - int len; - while ((len = input.read(array)) > 0) { - out.write(array, 0, len); - } - out.closeEntry(); - } - } else { - saveEntry(entry, tmp.getAbsolutePath()); - } - File skl = new File(tmp.getAbsolutePath() + ".skl"); - Files.delete(Paths.get(skl.toURI())); - File xlf = new File(tmp.getAbsolutePath() + ".xlf"); - Files.delete(Paths.get(xlf.toURI())); - } else { - saveEntry(entry, tmp.getAbsolutePath()); - } - } catch (IOException e) { - // do nothing - saveEntry(entry, tmp.getAbsolutePath()); - } - Files.delete(Paths.get(tmp.toURI())); - } else { - // not an XML file - File tmp = File.createTempFile("zip", ".tmp"); - try (FileOutputStream output = new FileOutputStream(tmp.getAbsolutePath())) { - byte[] buf = new byte[1024]; - int len; - while ((len = in.read(buf)) > 0) { - output.write(buf, 0, len); - } - } - saveEntry(entry, tmp.getAbsolutePath()); - Files.delete(Paths.get(tmp.toURI())); - } - } - try { - in.close(); - out.close(); - } catch (IOException e) { - result.add("1"); - if (params.get("format").equals(FileFormats.OFF)) { - result.add("Selected file is not a Microsoft Office 2007 document."); - } else { - result.add("Wrong document type."); - } - return result; - } - // sort the slides if it is PPTX - - if (params.get("format").equals(FileFormats.OFF) && isPPTX) { - sortSlides(); - } - - // output final XLIFF - - XMLOutputter outputter = new XMLOutputter(); - mergedRoot.addContent("\n"); - outputter.preserveSpace(true); - try (FileOutputStream output = new FileOutputStream(xliff)) { - outputter.output(merged, output); - } - result.add("0"); - } catch (IOException | SAXException | ParserConfigurationException e) { - Logger logger = System.getLogger(Office2Xliff.class.getName()); - logger.log(Level.ERROR, "Error converting Office file", e); - result.add("1"); - result.add(e.getMessage()); - } - return result; - } - - private static void cleanTags(String file, String catalog) - throws SAXException, IOException, ParserConfigurationException { - SAXBuilder builder = new SAXBuilder(); - builder.setEntityResolver(new Catalog(catalog)); - Document doc = builder.build(file); - Element root = doc.getRootElement(); - recurseCleaning(root); - XMLOutputter outputter = new XMLOutputter(); - try (FileOutputStream output = new FileOutputStream(file)) { - outputter.output(doc, output); - } - } - - private static void recurseCleaning(Element e) { - if (!e.getChildren("text:s").isEmpty()) { - Vector newContent = new Vector<>(); - List content = e.getContent(); - Iterator it = content.iterator(); - while (it.hasNext()) { - XMLNode n = it.next(); - if (n.getNodeType() == XMLNode.ELEMENT_NODE) { - Element e1 = (Element) n; - if (e1.getName().equals("text:s")) { - newContent.add(new TextNode(" ")); - } else { - newContent.add(n); - } - } else { - newContent.add(n); - } - } - e.setContent(newContent); - } else { - List list = e.getChildren(); - Iterator it = list.iterator(); - while (it.hasNext()) { - recurseCleaning(it.next()); - } - } - } - - private static void sortSlides() { - List files = mergedRoot.getChildren("file"); - List instructions = mergedRoot.getPI(); - Hashtable table = new Hashtable<>(); - TreeSet tree = new TreeSet<>(); - for (int i = 0; i < files.size(); i++) { - String key = padKey(getKey(files.get(i))); - tree.add(key); - table.put(key, "" + i); - } - Vector v = new Vector<>(); - Iterator it = tree.iterator(); - while (it.hasNext()) { - String key = it.next(); - v.add(files.get(Integer.parseInt(table.get(key)))); - } - mergedRoot.setContent(v); - Iterator pit = instructions.iterator(); - while (pit.hasNext()) { - mergedRoot.addContent(pit.next()); - } - } - - private static String padKey(String key) { - String path = key.substring(0, key.lastIndexOf('/')); - String file = key.substring(key.lastIndexOf('/')); - String name = ""; - String number = ""; - String extension = ""; - int i = 0; - for (i = 0; i < file.length(); i++) { - if (Character.isDigit(file.charAt(i))) { - break; - } - name = name + file.charAt(i); - } - for (; i < file.length(); i++) { - if (Character.isDigit(file.charAt(i))) { - number = number + file.charAt(i); - } else { - break; - } - } - while (number.length() < 5) { - number = "0" + number; - } - for (; i < file.length(); i++) { - if (Character.isDigit(file.charAt(i))) { - break; - } - extension = extension + file.charAt(i); - } - if (name.equals("/slide")) { - name = "aaa"; - } - if (name.equals("/notesSlide")) { - name = "bbb"; - } - if (!number.equals("00000")) { - return number + name + extension; - } - return path + file; - } - - private static String getKey(Element file) { - List groups = file.getChild("header").getChildren("prop-group"); - for (int i = 0; i < groups.size(); i++) { - Element group = groups.get(i); - if (group.getAttributeValue("name").equals("document")) { - return group.getChild("prop").getText(); - } - } - return null; - } - - private static int countSegments(String string) throws SAXException, IOException, ParserConfigurationException { - SAXBuilder builder = new SAXBuilder(); - Document doc = builder.build(string); - Element root = doc.getRootElement(); - return root.getChild("file").getChild("body").getChildren("trans-unit").size(); - } - - private static void saveEntry(ZipEntry entry, String name) throws IOException { - ZipEntry content = new ZipEntry(entry.getName()); - content.setMethod(ZipEntry.DEFLATED); - out.putNextEntry(content); - try (FileInputStream input = new FileInputStream(name)) { - byte[] array = new byte[1024]; - int len; - while ((len = input.read(array)) > 0) { - out.write(array, 0, len); - } - out.closeEntry(); - } - } - - private static void addFile(String xliff) throws SAXException, IOException, ParserConfigurationException { - SAXBuilder builder = new SAXBuilder(); - Document doc = builder.build(xliff); - Element root = doc.getRootElement(); - Element file = root.getChild("file"); - Element newFile = new Element("file"); - newFile.clone(file); - List pi = root.getPI(); - for (int i=0 ; i run(Hashtable params) { + Vector result = new Vector<>(); + + inputFile = params.get("source"); + String xliff = params.get("xliff"); + skeleton = params.get("skeleton"); + String catalog = params.get("catalog"); + + try { + + Document merged = new Document(null, "xliff", null, null); + mergedRoot = merged.getRootElement(); + mergedRoot.setAttribute("version", "1.2"); + mergedRoot.setAttribute("xmlns", "urn:oasis:names:tc:xliff:document:1.2"); + mergedRoot.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); + mergedRoot.setAttribute("xsi:schemaLocation", + "urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd"); + mergedRoot.addContent("\n"); + + try { + out = new ZipOutputStream(new FileOutputStream(skeleton)); + in = new ZipInputStream(new FileInputStream(inputFile)); + } catch (IOException e) { + result.add(Constants.ERROR); + if (params.get("format").equals(FileFormats.OFF)) { + result.add("Selected file is not a Microsoft Office 2007 document."); + } else { + result.add("Wrong document type."); + } + return result; + } + ZipEntry entry = null; + while ((entry = in.getNextEntry()) != null) { + if (entry.getName().matches(".*\\.[xX][mM][lL]") && !(entry.getName().matches(".*slideMaster.*") + || entry.getName().matches(".*slideLayout.*") || entry.getName().matches(".*handoutMaster.*") + || entry.getName().matches(".*notesMaster.*"))) { + File f = new File(entry.getName()); + String name = f.getName(); + File tmp = File.createTempFile(name.substring(0, name.lastIndexOf('.')), ".xml"); + try (FileOutputStream output = new FileOutputStream(tmp.getAbsolutePath())) { + byte[] buf = new byte[1024]; + int len; + while ((len = in.read(buf)) > 0) { + output.write(buf, 0, len); + } + } + if (name.equals("content.xml")) { + cleanTags(tmp.getAbsolutePath(), catalog); + } + try { + Hashtable table = new Hashtable<>(); + table.put("source", tmp.getAbsolutePath()); + table.put("xliff", tmp.getAbsolutePath() + ".xlf"); + table.put("skeleton", tmp.getAbsolutePath() + ".skl"); + table.put("catalog", params.get("catalog")); + table.put("srcLang", params.get("srcLang")); + String tgtLang = params.get("tgtLang"); + if (tgtLang != null) { + table.put("tgtLang", tgtLang); + } + table.put("srcEncoding", params.get("srcEncoding")); + table.put("paragraph", params.get("paragraph")); + table.put("srxFile", params.get("srxFile")); + table.put("format", params.get("format")); + Vector res = null; + if (params.get("format").equals(FileFormats.OFF)) { + res = MSOffice2Xliff.run(table); + if (tmp.getName().indexOf("slide") != -1) { + isPPTX = true; + } + } else { + res = Xml2Xliff.run(table); + } + if (Constants.SUCCESS.equals(res.get(0))) { + if (countSegments(tmp.getAbsolutePath() + ".xlf") > 0) { + updateXliff(tmp.getAbsolutePath() + ".xlf", entry.getName()); + addFile(tmp.getAbsolutePath() + ".xlf"); + ZipEntry content = new ZipEntry(entry.getName() + ".skl"); + content.setMethod(ZipEntry.DEFLATED); + out.putNextEntry(content); + try (FileInputStream input = new FileInputStream(tmp.getAbsolutePath() + ".skl")) { + byte[] array = new byte[1024]; + int len; + while ((len = input.read(array)) > 0) { + out.write(array, 0, len); + } + out.closeEntry(); + } + } else { + saveEntry(entry, tmp.getAbsolutePath()); + } + File skl = new File(tmp.getAbsolutePath() + ".skl"); + Files.delete(Paths.get(skl.toURI())); + File xlf = new File(tmp.getAbsolutePath() + ".xlf"); + Files.delete(Paths.get(xlf.toURI())); + } else { + saveEntry(entry, tmp.getAbsolutePath()); + } + } catch (IOException e) { + // do nothing + saveEntry(entry, tmp.getAbsolutePath()); + } + Files.delete(Paths.get(tmp.toURI())); + } else { + // not an XML file + File tmp = File.createTempFile("zip", ".tmp"); + try (FileOutputStream output = new FileOutputStream(tmp.getAbsolutePath())) { + byte[] buf = new byte[1024]; + int len; + while ((len = in.read(buf)) > 0) { + output.write(buf, 0, len); + } + } + saveEntry(entry, tmp.getAbsolutePath()); + Files.delete(Paths.get(tmp.toURI())); + } + } + try { + in.close(); + out.close(); + } catch (IOException e) { + result.add(Constants.ERROR); + if (params.get("format").equals(FileFormats.OFF)) { + result.add("Selected file is not a Microsoft Office 2007 document."); + } else { + result.add("Wrong document type."); + } + return result; + } + // sort the slides if it is PPTX + + if (params.get("format").equals(FileFormats.OFF) && isPPTX) { + sortSlides(); + } + + // output final XLIFF + + XMLOutputter outputter = new XMLOutputter(); + mergedRoot.addContent("\n"); + outputter.preserveSpace(true); + try (FileOutputStream output = new FileOutputStream(xliff)) { + outputter.output(merged, output); + } + result.add(Constants.SUCCESS); + } catch (IOException | SAXException | ParserConfigurationException e) { + Logger logger = System.getLogger(Office2Xliff.class.getName()); + logger.log(Level.ERROR, "Error converting Office file", e); + result.add(Constants.ERROR); + result.add(e.getMessage()); + } + return result; + } + + private static void cleanTags(String file, String catalog) + throws SAXException, IOException, ParserConfigurationException { + SAXBuilder builder = new SAXBuilder(); + builder.setEntityResolver(new Catalog(catalog)); + Document doc = builder.build(file); + Element root = doc.getRootElement(); + recurseCleaning(root); + XMLOutputter outputter = new XMLOutputter(); + try (FileOutputStream output = new FileOutputStream(file)) { + outputter.output(doc, output); + } + } + + private static void recurseCleaning(Element e) { + if (!e.getChildren("text:s").isEmpty()) { + Vector newContent = new Vector<>(); + List content = e.getContent(); + Iterator it = content.iterator(); + while (it.hasNext()) { + XMLNode n = it.next(); + if (n.getNodeType() == XMLNode.ELEMENT_NODE) { + Element e1 = (Element) n; + if (e1.getName().equals("text:s")) { + newContent.add(new TextNode(" ")); + } else { + newContent.add(n); + } + } else { + newContent.add(n); + } + } + e.setContent(newContent); + } else { + List list = e.getChildren(); + Iterator it = list.iterator(); + while (it.hasNext()) { + recurseCleaning(it.next()); + } + } + } + + private static void sortSlides() { + List files = mergedRoot.getChildren("file"); + List instructions = mergedRoot.getPI(); + Hashtable table = new Hashtable<>(); + TreeSet tree = new TreeSet<>(); + for (int i = 0; i < files.size(); i++) { + String key = padKey(getKey(files.get(i))); + tree.add(key); + table.put(key, "" + i); + } + Vector v = new Vector<>(); + Iterator it = tree.iterator(); + while (it.hasNext()) { + String key = it.next(); + v.add(files.get(Integer.parseInt(table.get(key)))); + } + mergedRoot.setContent(v); + Iterator pit = instructions.iterator(); + while (pit.hasNext()) { + mergedRoot.addContent(pit.next()); + } + } + + private static String padKey(String key) { + String path = key.substring(0, key.lastIndexOf('/')); + String file = key.substring(key.lastIndexOf('/')); + String name = ""; + String number = ""; + String extension = ""; + int i = 0; + for (i = 0; i < file.length(); i++) { + if (Character.isDigit(file.charAt(i))) { + break; + } + name = name + file.charAt(i); + } + for (; i < file.length(); i++) { + if (Character.isDigit(file.charAt(i))) { + number = number + file.charAt(i); + } else { + break; + } + } + while (number.length() < 5) { + number = "0" + number; + } + for (; i < file.length(); i++) { + if (Character.isDigit(file.charAt(i))) { + break; + } + extension = extension + file.charAt(i); + } + if (name.equals("/slide")) { + name = "aaa"; + } + if (name.equals("/notesSlide")) { + name = "bbb"; + } + if (!number.equals("00000")) { + return number + name + extension; + } + return path + file; + } + + private static String getKey(Element file) { + List groups = file.getChild("header").getChildren("prop-group"); + for (int i = 0; i < groups.size(); i++) { + Element group = groups.get(i); + if (group.getAttributeValue("name").equals("document")) { + return group.getChild("prop").getText(); + } + } + return null; + } + + private static int countSegments(String string) throws SAXException, IOException, ParserConfigurationException { + SAXBuilder builder = new SAXBuilder(); + Document doc = builder.build(string); + Element root = doc.getRootElement(); + return root.getChild("file").getChild("body").getChildren("trans-unit").size(); + } + + private static void saveEntry(ZipEntry entry, String name) throws IOException { + ZipEntry content = new ZipEntry(entry.getName()); + content.setMethod(ZipEntry.DEFLATED); + out.putNextEntry(content); + try (FileInputStream input = new FileInputStream(name)) { + byte[] array = new byte[1024]; + int len; + while ((len = input.read(array)) > 0) { + out.write(array, 0, len); + } + out.closeEntry(); + } + } + + private static void addFile(String xliff) throws SAXException, IOException, ParserConfigurationException { + SAXBuilder builder = new SAXBuilder(); + Document doc = builder.build(xliff); + Element root = doc.getRootElement(); + Element file = root.getChild("file"); + Element newFile = new Element("file"); + newFile.clone(file); + List pi = root.getPI(); + for (int i=0 ; i filesTable; - private static boolean isEmbedded = false; - - private Xliff2Office() { - // do not instantiate this class - // use run method instead - } - - public static Vector run(Hashtable params) { - Vector result = new Vector<>(); - String xliffFile = params.get("xliff"); - String outputFile = params.get("backfile"); - String catalog = params.get("catalog"); - filesTable = new Hashtable<>(); - try { - SAXBuilder builder = new SAXBuilder(); - Document doc = builder.build(xliffFile); - Element root = doc.getRootElement(); - List files = root.getChildren("file"); - Iterator it = files.iterator(); - while (it.hasNext()) { - saveFile(it.next(), xliffFile); - } - String skeleton = params.get("skeleton"); - if (isEmbedded) { - File t = new File(skeleton); - t.deleteOnExit(); - } - File f = new File(outputFile); - File p = f.getParentFile(); - if (p == null) { - p = new File(System.getProperty("user.dir")); - } - if (!p.exists()) { - p.mkdirs(); - } - if (!f.exists()) { - Files.createFile(Paths.get(f.toURI())); - } - try (ZipInputStream in = new ZipInputStream(new FileInputStream(skeleton))) { - try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(f))) { - ZipEntry entry = null; - while ((entry = in.getNextEntry()) != null) { - if (entry.getName().matches(".*\\.[xX][mM][lL]\\.skl")) { - String name = entry.getName().substring(0, entry.getName().lastIndexOf(".skl")); - File tmp = new File(filesTable.get(name) + ".skl"); - try (FileOutputStream output = new FileOutputStream(tmp.getAbsolutePath())) { - byte[] buf = new byte[1024]; - int len; - while ((len = in.read(buf)) > 0) { - output.write(buf, 0, len); - } - } - Hashtable table = new Hashtable<>(); - String s = filesTable.get(name); - if (s == null) { - LOGGER.log(Level.WARNING, "Skeleton not found for file " + name); - continue; - } - table.put("xliff", s); - table.put("backfile", filesTable.get(name) + ".xml"); - table.put("catalog", params.get("catalog")); - table.put("skeleton", filesTable.get(name) + ".skl"); - table.put("encoding", params.get("encoding")); - Vector res = Xliff2Xml.run(table); - if (!"0".equals(res.get(0))) { - return res; - } - // adjust the spaces in the file - fixSpaces(filesTable.get(name) + ".xml", catalog); - ZipEntry content = new ZipEntry(name); - content.setMethod(ZipEntry.DEFLATED); - out.putNextEntry(content); - try (FileInputStream input = new FileInputStream(filesTable.get(name) + ".xml")) { - byte[] buf = new byte[1024]; - int len; - while ((len = input.read(buf)) > 0) { - out.write(buf, 0, len); - } - out.closeEntry(); - } - tmp.deleteOnExit(); - File xml = new File(filesTable.get(name) + ".xml"); - Files.delete(Paths.get(xml.toURI())); - File xlf = new File(filesTable.get(name)); - Files.delete(Paths.get(xlf.toURI())); - } else { - File tmp = File.createTempFile("entry", ".tmp"); - try (FileOutputStream output = new FileOutputStream(tmp.getAbsolutePath())) { - byte[] buf = new byte[1024]; - int len; - while ((len = in.read(buf)) > 0) { - output.write(buf, 0, len); - } - } - ZipEntry content = new ZipEntry(entry.getName()); - content.setMethod(ZipEntry.DEFLATED); - out.putNextEntry(content); - try (FileInputStream input = new FileInputStream(tmp.getAbsolutePath())) { - byte[] buf = new byte[1024]; - int len; - while ((len = input.read(buf)) > 0) { - out.write(buf, 0, len); - } - out.closeEntry(); - } - Files.delete(Paths.get(tmp.toURI())); - } - } - } - } - if (isEmbedded) { - File f1 = new File(skeleton); - Files.delete(Paths.get(f1.toURI())); - } - result.add("0"); - } catch (IOException | SAXException | ParserConfigurationException e) { - LOGGER.log(Level.ERROR, "Error converting Office file", e); - result.add("1"); - result.add(e.getMessage()); - } - return result; - } - - private static void fixSpaces(String file, String catalog) - throws SAXException, IOException, ParserConfigurationException { - SAXBuilder builder = new SAXBuilder(); - builder.setValidating(false); - Document doc = builder.build(file); - builder.setEntityResolver(new Catalog(catalog)); - addPreserveSpace(doc.getRootElement()); - XMLOutputter outputter = new XMLOutputter(); - try (FileOutputStream output = new FileOutputStream(file)) { - outputter.output(doc, output); - } - } - - private static void addPreserveSpace(Element e) { - // a:t is used in PowerPoint and should not be modified - if (e.getName().matches("[w-z]:t") || e.getName().equals("t")) { - e.setAttribute("xml:space", "preserve"); - } - List children = e.getChildren(); - Iterator it = children.iterator(); - while (it.hasNext()) { - addPreserveSpace(it.next()); - } - } - - private static void saveFile(Element element, String xliffFile) throws IOException { - Document doc = new Document(null, "xliff", null, null); - Element root = doc.getRootElement(); - root.setAttribute("version", "1.2"); - Element file = new Element("file"); - file.clone(element); - root.addContent(file); - File xliff = File.createTempFile("tmp", ".xlf", new File(xliffFile).getParentFile()); - List groups = file.getChild("header").getChildren("prop-group"); - Iterator i = groups.iterator(); - while (i.hasNext()) { - Element group = i.next(); - if (group.getAttributeValue("name").equals("document")) { - filesTable.put(group.getChild("prop").getText(), xliff.getAbsolutePath()); - } - } - if (file.getChild("header").getChild("skl").getChild("external-file") == null) { - // embedded skeleton - file.getChild("header").getChild("skl").addContent(new Element("external-file")); - isEmbedded = true; - } - file.getChild("header").getChild("skl").getChild("external-file").setAttribute("href", - xliff.getAbsolutePath() + ".skl"); //$NON-NLS-1$ - XMLOutputter outputter = new XMLOutputter(); - try (FileOutputStream output = new FileOutputStream(xliff.getAbsolutePath())) { - outputter.output(doc, output); - } - } - -} +/******************************************************************************* + * Copyright (c) 2003-2019 Maxprograms. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/org/documents/epl-v10.html + * + * Contributors: + * Maxprograms - initial API and implementation + *******************************************************************************/ +package com.maxprograms.converters.office; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Vector; +import java.lang.System.Logger.Level; +import java.lang.System.Logger; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +import javax.xml.parsers.ParserConfigurationException; + +import org.xml.sax.SAXException; + +import com.maxprograms.converters.Constants; +import com.maxprograms.converters.xml.Xliff2Xml; +import com.maxprograms.xml.Catalog; +import com.maxprograms.xml.Document; +import com.maxprograms.xml.Element; +import com.maxprograms.xml.SAXBuilder; +import com.maxprograms.xml.XMLOutputter; + +public class Xliff2Office { + + private static final Logger LOGGER = System.getLogger(Xliff2Office.class.getName()); + + private static Hashtable filesTable; + private static boolean isEmbedded = false; + + private Xliff2Office() { + // do not instantiate this class + // use run method instead + } + + public static Vector run(Hashtable params) { + Vector result = new Vector<>(); + String xliffFile = params.get("xliff"); + String outputFile = params.get("backfile"); + String catalog = params.get("catalog"); + filesTable = new Hashtable<>(); + try { + SAXBuilder builder = new SAXBuilder(); + Document doc = builder.build(xliffFile); + Element root = doc.getRootElement(); + List files = root.getChildren("file"); + Iterator it = files.iterator(); + while (it.hasNext()) { + saveFile(it.next(), xliffFile); + } + String skeleton = params.get("skeleton"); + if (isEmbedded) { + File t = new File(skeleton); + t.deleteOnExit(); + } + File f = new File(outputFile); + File p = f.getParentFile(); + if (p == null) { + p = new File(System.getProperty("user.dir")); + } + if (!p.exists()) { + p.mkdirs(); + } + if (!f.exists()) { + Files.createFile(Paths.get(f.toURI())); + } + try (ZipInputStream in = new ZipInputStream(new FileInputStream(skeleton))) { + try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(f))) { + ZipEntry entry = null; + while ((entry = in.getNextEntry()) != null) { + if (entry.getName().matches(".*\\.[xX][mM][lL]\\.skl")) { + String name = entry.getName().substring(0, entry.getName().lastIndexOf(".skl")); + File tmp = new File(filesTable.get(name) + ".skl"); + try (FileOutputStream output = new FileOutputStream(tmp.getAbsolutePath())) { + byte[] buf = new byte[1024]; + int len; + while ((len = in.read(buf)) > 0) { + output.write(buf, 0, len); + } + } + Hashtable table = new Hashtable<>(); + String s = filesTable.get(name); + if (s == null) { + LOGGER.log(Level.WARNING, "Skeleton not found for file " + name); + continue; + } + table.put("xliff", s); + table.put("backfile", filesTable.get(name) + ".xml"); + table.put("catalog", params.get("catalog")); + table.put("skeleton", filesTable.get(name) + ".skl"); + table.put("encoding", params.get("encoding")); + Vector res = Xliff2Xml.run(table); + if (!Constants.SUCCESS.equals(res.get(0))) { + return res; + } + // adjust the spaces in the file + fixSpaces(filesTable.get(name) + ".xml", catalog); + ZipEntry content = new ZipEntry(name); + content.setMethod(ZipEntry.DEFLATED); + out.putNextEntry(content); + try (FileInputStream input = new FileInputStream(filesTable.get(name) + ".xml")) { + byte[] buf = new byte[1024]; + int len; + while ((len = input.read(buf)) > 0) { + out.write(buf, 0, len); + } + out.closeEntry(); + } + tmp.deleteOnExit(); + File xml = new File(filesTable.get(name) + ".xml"); + Files.delete(Paths.get(xml.toURI())); + File xlf = new File(filesTable.get(name)); + Files.delete(Paths.get(xlf.toURI())); + } else { + File tmp = File.createTempFile("entry", ".tmp"); + try (FileOutputStream output = new FileOutputStream(tmp.getAbsolutePath())) { + byte[] buf = new byte[1024]; + int len; + while ((len = in.read(buf)) > 0) { + output.write(buf, 0, len); + } + } + ZipEntry content = new ZipEntry(entry.getName()); + content.setMethod(ZipEntry.DEFLATED); + out.putNextEntry(content); + try (FileInputStream input = new FileInputStream(tmp.getAbsolutePath())) { + byte[] buf = new byte[1024]; + int len; + while ((len = input.read(buf)) > 0) { + out.write(buf, 0, len); + } + out.closeEntry(); + } + Files.delete(Paths.get(tmp.toURI())); + } + } + } + } + if (isEmbedded) { + File f1 = new File(skeleton); + Files.delete(Paths.get(f1.toURI())); + } + result.add(Constants.SUCCESS); + } catch (IOException | SAXException | ParserConfigurationException e) { + LOGGER.log(Level.ERROR, "Error converting Office file", e); + result.add(Constants.ERROR); + result.add(e.getMessage()); + } + return result; + } + + private static void fixSpaces(String file, String catalog) + throws SAXException, IOException, ParserConfigurationException { + SAXBuilder builder = new SAXBuilder(); + builder.setValidating(false); + Document doc = builder.build(file); + builder.setEntityResolver(new Catalog(catalog)); + addPreserveSpace(doc.getRootElement()); + XMLOutputter outputter = new XMLOutputter(); + try (FileOutputStream output = new FileOutputStream(file)) { + outputter.output(doc, output); + } + } + + private static void addPreserveSpace(Element e) { + // a:t is used in PowerPoint and should not be modified + if (e.getName().matches("[w-z]:t") || e.getName().equals("t")) { + e.setAttribute("xml:space", "preserve"); + } + List children = e.getChildren(); + Iterator it = children.iterator(); + while (it.hasNext()) { + addPreserveSpace(it.next()); + } + } + + private static void saveFile(Element element, String xliffFile) throws IOException { + Document doc = new Document(null, "xliff", null, null); + Element root = doc.getRootElement(); + root.setAttribute("version", "1.2"); + Element file = new Element("file"); + file.clone(element); + root.addContent(file); + File xliff = File.createTempFile("tmp", ".xlf", new File(xliffFile).getParentFile()); + List groups = file.getChild("header").getChildren("prop-group"); + Iterator i = groups.iterator(); + while (i.hasNext()) { + Element group = i.next(); + if (group.getAttributeValue("name").equals("document")) { + filesTable.put(group.getChild("prop").getText(), xliff.getAbsolutePath()); + } + } + if (file.getChild("header").getChild("skl").getChild("external-file") == null) { + // embedded skeleton + file.getChild("header").getChild("skl").addContent(new Element("external-file")); + isEmbedded = true; + } + file.getChild("header").getChild("skl").getChild("external-file").setAttribute("href", + xliff.getAbsolutePath() + ".skl"); //$NON-NLS-1$ + XMLOutputter outputter = new XMLOutputter(); + try (FileOutputStream output = new FileOutputStream(xliff.getAbsolutePath())) { + outputter.output(doc, output); + } + } + +} diff --git a/src/com/maxprograms/converters/plaintext/Text2Xliff.java b/src/com/maxprograms/converters/plaintext/Text2Xliff.java index e053215f..ef15b149 100644 --- a/src/com/maxprograms/converters/plaintext/Text2Xliff.java +++ b/src/com/maxprograms/converters/plaintext/Text2Xliff.java @@ -1,188 +1,189 @@ -/******************************************************************************* - * Copyright (c) 2003-2019 Maxprograms. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 1.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/org/documents/epl-v10.html - * - * Contributors: - * Maxprograms - initial API and implementation - *******************************************************************************/ -/* - * Created on Jun 2, 2004 - * - */ -package com.maxprograms.converters.plaintext; - -import java.io.BufferedReader; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.util.Hashtable; -import java.util.Vector; -import java.lang.System.Logger.Level; -import java.lang.System.Logger; - -import javax.xml.parsers.ParserConfigurationException; - -import org.xml.sax.SAXException; - -import com.maxprograms.converters.Utils; -import com.maxprograms.segmenter.Segmenter; - -public class Text2Xliff { - - private static FileOutputStream output; - private static FileOutputStream skeleton; - private static String source; - private static String sourceLanguage; - private static int segId; - private static Segmenter segmenter; - private static boolean segByElement; - - private Text2Xliff() { - // do not instantiate this class - // use run method instead - } - - public static Vector run(Hashtable params) { - Vector result = new Vector<>(); - segId = 0; - - String inputFile = params.get("source"); - String xliffFile = params.get("xliff"); - String skeletonFile = params.get("skeleton"); - sourceLanguage = params.get("srcLang"); - String targetLanguage = params.get("tgtLang"); - String srcEncoding = params.get("srcEncoding"); - String elementSegmentation = params.get("paragraph"); - boolean breakOnCRLF = "yes".equals(params.get("breakOnCRLF")); - String catalog = params.get("catalog"); - String tgtLang = ""; - if (targetLanguage != null) { - tgtLang = "\" target-language=\"" + targetLanguage; - } - - if (elementSegmentation == null) { - segByElement = false; - } else { - if (elementSegmentation.equals("yes")) { - segByElement = true; - } else { - segByElement = false; - } - } - - source = ""; - try { - if (!segByElement) { - String initSegmenter = params.get("srxFile"); - segmenter = new Segmenter(initSegmenter, sourceLanguage, catalog); - } - FileInputStream stream = new FileInputStream(inputFile); - try (InputStreamReader input = new InputStreamReader(stream, srcEncoding)) { - BufferedReader buffer = new BufferedReader(input); - - output = new FileOutputStream(xliffFile); - - writeString("\n"); - writeString("\n"); - writeString("\n"); - - writeString("\n"); - writeString("
\n"); - writeString(" \n"); - writeString(" \n"); - writeString(" \n"); - writeString("
\n"); - writeString("\n"); - - skeleton = new FileOutputStream(skeletonFile); - - if (breakOnCRLF) { - source = buffer.readLine(); - while (source != null) { - if (source.trim().length() == 0) { - writeSkeleton(source + "\n"); - } else { - writeSegment(); - } - source = buffer.readLine(); - } - } else { - String line = buffer.readLine(); - while (line != null) { - line = line + "\n"; - - if (line.trim().length() == 0) { - // no text in this line - // segment separator - writeSkeleton(line); - } else { - while (line != null && line.trim().length() != 0) { - source = source + line; - line = buffer.readLine(); - if (line != null) { - line = line + "\n"; - } - } - writeSegment(); - } - line = buffer.readLine(); - } - } - skeleton.close(); - - writeString("\n"); - writeString("
\n"); - writeString("
"); - } - output.close(); - result.add("0"); // success - } catch (IOException | SAXException | ParserConfigurationException e) { - Logger logger = System.getLogger(Text2Xliff.class.getName()); - logger.log(Level.ERROR, "Error converting TEXT file", e); - result.add("1"); - result.add(e.getMessage()); - } - - return result; - } - - private static void writeString(String string) throws IOException { - output.write(string.getBytes(StandardCharsets.UTF_8)); - } - - private static void writeSkeleton(String string) throws IOException { - skeleton.write(string.getBytes(StandardCharsets.UTF_8)); - } - - private static void writeSegment() throws IOException { - String[] segments; - if (!segByElement) { - segments = segmenter.segment(source); - } else { - segments = new String[1]; - segments[0] = source; - } - for (int i = 0; i < segments.length; i++) { - if (Utils.cleanString(segments[i]).trim().equals("")) { - writeSkeleton(segments[i]); - } else { - writeString(" \n" - + " " + Utils.cleanString(segments[i]) - + "\n"); - writeString(" \n"); - writeSkeleton("%%%" + segId++ + "%%%\n"); - } - } - source = ""; - } - -} +/******************************************************************************* + * Copyright (c) 2003-2019 Maxprograms. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/org/documents/epl-v10.html + * + * Contributors: + * Maxprograms - initial API and implementation + *******************************************************************************/ +/* + * Created on Jun 2, 2004 + * + */ +package com.maxprograms.converters.plaintext; + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Hashtable; +import java.util.Vector; +import java.lang.System.Logger.Level; +import java.lang.System.Logger; + +import javax.xml.parsers.ParserConfigurationException; + +import org.xml.sax.SAXException; + +import com.maxprograms.converters.Constants; +import com.maxprograms.converters.Utils; +import com.maxprograms.segmenter.Segmenter; + +public class Text2Xliff { + + private static FileOutputStream output; + private static FileOutputStream skeleton; + private static String source; + private static String sourceLanguage; + private static int segId; + private static Segmenter segmenter; + private static boolean segByElement; + + private Text2Xliff() { + // do not instantiate this class + // use run method instead + } + + public static Vector run(Hashtable params) { + Vector result = new Vector<>(); + segId = 0; + + String inputFile = params.get("source"); + String xliffFile = params.get("xliff"); + String skeletonFile = params.get("skeleton"); + sourceLanguage = params.get("srcLang"); + String targetLanguage = params.get("tgtLang"); + String srcEncoding = params.get("srcEncoding"); + String elementSegmentation = params.get("paragraph"); + boolean breakOnCRLF = "yes".equals(params.get("breakOnCRLF")); + String catalog = params.get("catalog"); + String tgtLang = ""; + if (targetLanguage != null) { + tgtLang = "\" target-language=\"" + targetLanguage; + } + + if (elementSegmentation == null) { + segByElement = false; + } else { + if (elementSegmentation.equals("yes")) { + segByElement = true; + } else { + segByElement = false; + } + } + + source = ""; + try { + if (!segByElement) { + String initSegmenter = params.get("srxFile"); + segmenter = new Segmenter(initSegmenter, sourceLanguage, catalog); + } + FileInputStream stream = new FileInputStream(inputFile); + try (InputStreamReader input = new InputStreamReader(stream, srcEncoding)) { + BufferedReader buffer = new BufferedReader(input); + + output = new FileOutputStream(xliffFile); + + writeString("\n"); + writeString("\n"); + writeString("\n"); + + writeString("\n"); + writeString("
\n"); + writeString(" \n"); + writeString(" \n"); + writeString(" \n"); + writeString("
\n"); + writeString("\n"); + + skeleton = new FileOutputStream(skeletonFile); + + if (breakOnCRLF) { + source = buffer.readLine(); + while (source != null) { + if (source.trim().length() == 0) { + writeSkeleton(source + "\n"); + } else { + writeSegment(); + } + source = buffer.readLine(); + } + } else { + String line = buffer.readLine(); + while (line != null) { + line = line + "\n"; + + if (line.trim().length() == 0) { + // no text in this line + // segment separator + writeSkeleton(line); + } else { + while (line != null && line.trim().length() != 0) { + source = source + line; + line = buffer.readLine(); + if (line != null) { + line = line + "\n"; + } + } + writeSegment(); + } + line = buffer.readLine(); + } + } + skeleton.close(); + + writeString("\n"); + writeString("
\n"); + writeString("
"); + } + output.close(); + result.add(Constants.SUCCESS); + } catch (IOException | SAXException | ParserConfigurationException e) { + Logger logger = System.getLogger(Text2Xliff.class.getName()); + logger.log(Level.ERROR, "Error converting TEXT file", e); + result.add(Constants.ERROR); + result.add(e.getMessage()); + } + + return result; + } + + private static void writeString(String string) throws IOException { + output.write(string.getBytes(StandardCharsets.UTF_8)); + } + + private static void writeSkeleton(String string) throws IOException { + skeleton.write(string.getBytes(StandardCharsets.UTF_8)); + } + + private static void writeSegment() throws IOException { + String[] segments; + if (!segByElement) { + segments = segmenter.segment(source); + } else { + segments = new String[1]; + segments[0] = source; + } + for (int i = 0; i < segments.length; i++) { + if (Utils.cleanString(segments[i]).trim().equals("")) { + writeSkeleton(segments[i]); + } else { + writeString(" \n" + + " " + Utils.cleanString(segments[i]) + + "\n"); + writeString(" \n"); + writeSkeleton("%%%" + segId++ + "%%%\n"); + } + } + source = ""; + } + +} diff --git a/src/com/maxprograms/converters/plaintext/Xliff2Text.java b/src/com/maxprograms/converters/plaintext/Xliff2Text.java index 860e9f7d..6b04d85f 100644 --- a/src/com/maxprograms/converters/plaintext/Xliff2Text.java +++ b/src/com/maxprograms/converters/plaintext/Xliff2Text.java @@ -1,182 +1,182 @@ -/******************************************************************************* - * Copyright (c) 2003-2019 Maxprograms. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 1.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/org/documents/epl-v10.html - * - * Contributors: - * Maxprograms - initial API and implementation - *******************************************************************************/ -package com.maxprograms.converters.plaintext; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.List; -import java.util.Vector; -import java.lang.System.Logger.Level; -import java.lang.System.Logger; - -import javax.xml.parsers.ParserConfigurationException; - -import org.xml.sax.SAXException; - -import com.maxprograms.xml.Catalog; -import com.maxprograms.xml.Document; -import com.maxprograms.xml.Element; -import com.maxprograms.xml.SAXBuilder; -import com.maxprograms.xml.TextNode; -import com.maxprograms.xml.XMLNode; - -public class Xliff2Text { - - private static String xliffFile; - private static String encoding; - private static Hashtable segments; - private static Catalog catalog; - private static FileOutputStream output; - - private Xliff2Text() { - // do not instantiate this class - // use run method instead - } - - public static Vector run(Hashtable params) { - - Vector result = new Vector<>(); - - String sklFile = params.get("skeleton"); - xliffFile = params.get("xliff"); - encoding = params.get("encoding"); - - try { - catalog = new Catalog(params.get("catalog")); - String outputFile = params.get("backfile"); - File f = new File(outputFile); - File p = f.getParentFile(); - if (p == null) { - p = new File(System.getProperty("user.dir")); - } - if (!p.exists()) { - p.mkdirs(); - } - if (!f.exists()) { - Files.createFile(Paths.get(f.toURI())); - } - output = new FileOutputStream(f); - loadSegments(); - try (InputStreamReader input = new InputStreamReader(new FileInputStream(sklFile), - StandardCharsets.UTF_8)) { - BufferedReader buffer = new BufferedReader(input); - String line; - while ((line = buffer.readLine()) != null) { - line = line + "\n"; - - if (line.indexOf("%%%") != -1) { - // - // contains translatable text - // - int index = line.indexOf("%%%"); - while (index != -1) { - String start = line.substring(0, index); - writeString(start); - line = line.substring(index + 3); - String code = line.substring(0, line.indexOf("%%%")); - line = line.substring(line.indexOf("%%%") + 3); - Element segment = segments.get(code); - if (segment != null) { - Element target = segment.getChild("target"); - Element source = segment.getChild("source"); - if (target != null) { - if (segment.getAttributeValue("approved", "no").equals("yes")) { - writeString(extractText(target)); - } else { - writeString(extractText(source)); - } - } else { - writeString(extractText(source)); - } - } else { - result.add(0, "1"); - result.add(1, "segment " + code + " not found"); - return result; - } - - index = line.indexOf("%%%"); - if (index == -1) { - writeString(line); - } - } // end while - } else { - // - // non translatable portion - // - writeString(line); - } - } - } - output.close(); - result.add("0"); - - } catch (IOException | SAXException | ParserConfigurationException e) { - Logger logger = System.getLogger(Xliff2Text.class.getName()); - logger.log(Level.ERROR, "Error merging TEXT file", e); - result.add("1"); - result.add(e.getMessage()); - } - return result; - } - - private static String extractText(Element target) { - String result = ""; - List content = target.getContent(); - Iterator i = content.iterator(); - while (i.hasNext()) { - XMLNode n = i.next(); - if (n.getNodeType() == XMLNode.ELEMENT_NODE) { - Element e = (Element) n; - result = result + extractText(e); - } - if (n.getNodeType() == XMLNode.TEXT_NODE) { - result = result + ((TextNode) n).getText(); - } - } - return result; - } - - private static void loadSegments() throws SAXException, IOException, ParserConfigurationException { - - SAXBuilder builder = new SAXBuilder(); - if (catalog != null) { - builder.setEntityResolver(catalog); - } - - Document doc = builder.build(xliffFile); - Element root = doc.getRootElement(); - Element body = root.getChild("file").getChild("body"); - List units = body.getChildren("trans-unit"); - Iterator i = units.iterator(); - - segments = new Hashtable<>(); - - while (i.hasNext()) { - Element unit = i.next(); - segments.put(unit.getAttributeValue("id"), unit); - } - } - - private static void writeString(String string) throws IOException { - output.write(string.getBytes(encoding)); - } - -} +/******************************************************************************* + * Copyright (c) 2003-2019 Maxprograms. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/org/documents/epl-v10.html + * + * Contributors: + * Maxprograms - initial API and implementation + *******************************************************************************/ +package com.maxprograms.converters.plaintext; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Vector; +import java.lang.System.Logger.Level; +import java.lang.System.Logger; + +import javax.xml.parsers.ParserConfigurationException; + +import org.xml.sax.SAXException; + +import com.maxprograms.converters.Constants; +import com.maxprograms.xml.Catalog; +import com.maxprograms.xml.Document; +import com.maxprograms.xml.Element; +import com.maxprograms.xml.SAXBuilder; +import com.maxprograms.xml.TextNode; +import com.maxprograms.xml.XMLNode; + +public class Xliff2Text { + + private static String xliffFile; + private static String encoding; + private static Hashtable segments; + private static Catalog catalog; + private static FileOutputStream output; + + private Xliff2Text() { + // do not instantiate this class + // use run method instead + } + + public static Vector run(Hashtable params) { + + Vector result = new Vector<>(); + + String sklFile = params.get("skeleton"); + xliffFile = params.get("xliff"); + encoding = params.get("encoding"); + + try { + catalog = new Catalog(params.get("catalog")); + String outputFile = params.get("backfile"); + File f = new File(outputFile); + File p = f.getParentFile(); + if (p == null) { + p = new File(System.getProperty("user.dir")); + } + if (!p.exists()) { + p.mkdirs(); + } + if (!f.exists()) { + Files.createFile(Paths.get(f.toURI())); + } + output = new FileOutputStream(f); + loadSegments(); + try (InputStreamReader input = new InputStreamReader(new FileInputStream(sklFile), + StandardCharsets.UTF_8)) { + BufferedReader buffer = new BufferedReader(input); + String line; + while ((line = buffer.readLine()) != null) { + line = line + "\n"; + + if (line.indexOf("%%%") != -1) { + // + // contains translatable text + // + int index = line.indexOf("%%%"); + while (index != -1) { + String start = line.substring(0, index); + writeString(start); + line = line.substring(index + 3); + String code = line.substring(0, line.indexOf("%%%")); + line = line.substring(line.indexOf("%%%") + 3); + Element segment = segments.get(code); + if (segment != null) { + Element target = segment.getChild("target"); + Element source = segment.getChild("source"); + if (target != null) { + if (segment.getAttributeValue("approved", "no").equals("yes")) { + writeString(extractText(target)); + } else { + writeString(extractText(source)); + } + } else { + writeString(extractText(source)); + } + } else { + result.add(Constants.ERROR); + result.add("segment " + code + " not found"); + return result; + } + + index = line.indexOf("%%%"); + if (index == -1) { + writeString(line); + } + } // end while + } else { + // + // non translatable portion + // + writeString(line); + } + } + } + output.close(); + result.add(Constants.SUCCESS); + } catch (IOException | SAXException | ParserConfigurationException e) { + Logger logger = System.getLogger(Xliff2Text.class.getName()); + logger.log(Level.ERROR, "Error merging TEXT file", e); + result.add(Constants.ERROR); + result.add(e.getMessage()); + } + return result; + } + + private static String extractText(Element target) { + String result = ""; + List content = target.getContent(); + Iterator i = content.iterator(); + while (i.hasNext()) { + XMLNode n = i.next(); + if (n.getNodeType() == XMLNode.ELEMENT_NODE) { + Element e = (Element) n; + result = result + extractText(e); + } + if (n.getNodeType() == XMLNode.TEXT_NODE) { + result = result + ((TextNode) n).getText(); + } + } + return result; + } + + private static void loadSegments() throws SAXException, IOException, ParserConfigurationException { + + SAXBuilder builder = new SAXBuilder(); + if (catalog != null) { + builder.setEntityResolver(catalog); + } + + Document doc = builder.build(xliffFile); + Element root = doc.getRootElement(); + Element body = root.getChild("file").getChild("body"); + List units = body.getChildren("trans-unit"); + Iterator i = units.iterator(); + + segments = new Hashtable<>(); + + while (i.hasNext()) { + Element unit = i.next(); + segments.put(unit.getAttributeValue("id"), unit); + } + } + + private static void writeString(String string) throws IOException { + output.write(string.getBytes(encoding)); + } + +} diff --git a/src/com/maxprograms/converters/po/Po2Xliff.java b/src/com/maxprograms/converters/po/Po2Xliff.java index eca936fc..94bb0c39 100644 --- a/src/com/maxprograms/converters/po/Po2Xliff.java +++ b/src/com/maxprograms/converters/po/Po2Xliff.java @@ -1,488 +1,489 @@ -/******************************************************************************* - * Copyright (c) 2003-2019 Maxprograms. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 1.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/org/documents/epl-v10.html - * - * Contributors: - * Maxprograms - initial API and implementation - *******************************************************************************/ - -package com.maxprograms.converters.po; - -import java.io.BufferedReader; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.util.Hashtable; -import java.util.StringTokenizer; -import java.util.Vector; -import java.lang.System.Logger.Level; -import java.lang.System.Logger; - -import com.maxprograms.converters.Utils; - -public class Po2Xliff { - - private static FileOutputStream output; - private static FileOutputStream skeleton; - - private static String source; - private static String target; - private static String comment; - private static String context; - private static String reference; - private static String flags; - private static boolean fuzzy; - private static boolean cformat; - - private static String sourceLanguage; - private static int segId; - private static int domainId; - private static int contextId = 1; - private static int refId = 1; - private static String newContext; - private static Vector pluralTargets; - private static int plurals; - private static String plural_source; - - private Po2Xliff() { - // do not instantiate this class - // use run method instead - } - - public static Vector run(Hashtable params) { - Vector result = new Vector<>(); - - String inputFile = params.get("source"); - String xliffFile = params.get("xliff"); - String skeletonFile = params.get("skeleton"); - sourceLanguage = params.get("srcLang"); - String targetLanguage = params.get("tgtLang"); - String srcEncoding = params.get("srcEncoding"); - String tgtLang = ""; - if (targetLanguage != null) { - tgtLang = "\" target-language=\"" + targetLanguage; - } - - source = ""; - plural_source = ""; - pluralTargets = new Vector<>(); - target = ""; - comment = ""; - context = ""; - reference = ""; - flags = ""; - newContext = ""; - fuzzy = false; - boolean inDomain = false; - cformat = false; - - try { - try (FileInputStream stream = new FileInputStream(inputFile)) { - try (InputStreamReader input = new InputStreamReader(stream, srcEncoding)) { - BufferedReader buffer = new BufferedReader(input); - - output = new FileOutputStream(xliffFile); - - writeString("\n"); - writeString("\n"); - writeString("\n"); - - writeString("\n"); - writeString("
\n"); - writeString(" \n"); - writeString(" \n"); - writeString(" \n"); - writeString("
\n"); - writeString("\n"); - - skeleton = new FileOutputStream(skeletonFile); - - String line; - while ((line = buffer.readLine()) != null) { - line = line + "\n"; - - if (line.trim().length() == 0) { - // no text in this line - // segment separator - writeSkeleton(line); - } else { - if (line.startsWith("#:")) { - // it is a reference - if (reference.equals("")) { - reference = line.substring(2); - } else { - reference = reference + " " + line.substring(2); - } - } - if (line.startsWith("# ")) { - // translator comment - comment = comment + line.substring(2); - } - if (line.trim().equals("#")) { - comment = comment + "\n"; - } - if (line.startsWith("#.")) { - // automatic comment - context = context + line.substring(2); - } - if (line.startsWith("#,")) { - flags = line.substring(2); - // check for fuzzy - if (flags.indexOf("fuzzy") != -1) { - fuzzy = true; - } - // Only c-format is parsed. Tags from other - // formats, like php-format or python-format, - // are left as part of the text - if (flags.indexOf("c-format") != -1 && flags.indexOf("no-c-format") == -1) { - cformat = true; - } - } - if (line.startsWith("#~")) { - // commented entry - writeSkeleton(line); - } - if (line.startsWith("msgctxt")) { - // get context text - line = line.substring(5); - newContext = line.substring(line.indexOf('\"') + 1, line.lastIndexOf('\"')); - line = buffer.readLine(); - while (line.startsWith("\"")) { - newContext = newContext + "\n" - + line.substring(line.indexOf('\"') + 1, line.lastIndexOf('\"')); - line = buffer.readLine(); - } - continue; - } - if (line.startsWith("msgid")) { - if (line.startsWith("msgid_plural")) { - // get plural source - line = line.substring(12); - plural_source = line.substring(line.indexOf('\"') + 1, line.lastIndexOf('\"')); - line = buffer.readLine(); - while (line.startsWith("\"")) { - plural_source = plural_source + "\n" - + line.substring(line.indexOf('\"') + 1, line.lastIndexOf('\"')); - line = buffer.readLine(); - } - } else { - // get source text - line = line.substring(5); - source = line.substring(line.indexOf('\"') + 1, line.lastIndexOf('\"')); - line = buffer.readLine(); - while (line.trim().startsWith("\"")) { - source = source + "\n" - + line.substring(line.indexOf('\"') + 1, line.lastIndexOf('\"')); - line = buffer.readLine(); - } - } - continue; - } - if (line.startsWith("msgstr")) { - if (line.startsWith("msgstr[")) { - while (line.startsWith("msgstr[")) { - // get all plural targets - line = line.substring(line.indexOf(']') + 1); - String pluralTarget = line.substring(line.indexOf('\"') + 1, - line.lastIndexOf('\"')); - line = buffer.readLine(); - while (line != null && line.trim().startsWith("\"")) { - pluralTarget = pluralTarget + "\n" - + line.substring(line.indexOf('\"') + 1, line.lastIndexOf('\"')); - line = buffer.readLine(); - if (line == null) { - line = ""; - } - } - if (line == null) { - line = ""; - } - pluralTargets.add(pluralTarget); - } - writeSegment(); - } else { - // get the target - line = line.substring(6); - target = line.substring(line.indexOf('\"') + 1, line.lastIndexOf('\"')); - line = buffer.readLine(); - while (line != null && line.trim().startsWith("\"")) { - target = target + "\n" - + line.substring(line.indexOf('\"') + 1, line.lastIndexOf('\"')); - line = buffer.readLine(); - if (line == null) { - line = ""; - } - if (line.trim().startsWith("\"Plural-Forms:")) { - parsePlural(line); - } - } - if (plural_source.equals("")) { - writeSegment(); - } - } - continue; - } - if (line.startsWith("domain")) { - if (inDomain) { - writeString(" \n"); - } - inDomain = true; - writeString( - " \n"); - writeSkeleton(line); - } - } - } - - skeleton.close(); - - if (inDomain) { - writeString(" \n"); - } - writeString("\n"); - writeString("
\n"); - writeString("
"); - output.close(); - } - } - result.add(0, "0"); // success - } catch (IOException e) { - Logger logger = System.getLogger(Po2Xliff.class.getName()); - logger.log(Level.ERROR, "Error converting PO file", e); - result.add(0, "1"); - result.add(1, e.getMessage()); - } - - return result; - } - - private static void parsePlural(String line) { - String string = line.substring(line.indexOf("nplurals") + 8).trim(); - String number = string.substring(string.indexOf('=') + 1, string.indexOf(';')); - plurals = Integer.parseInt(number); - } - - private static void writeString(String string) throws IOException { - output.write(string.getBytes(StandardCharsets.UTF_8)); - } - - private static void writeSkeleton(String string) throws IOException { - skeleton.write(string.getBytes(StandardCharsets.UTF_8)); - } - - private static void writeSegment() throws IOException { - if (!plural_source.equals("")) { - writeString(" \n"); - if (!context.equals("")) { - writeString(" \n" + " " - + Utils.cleanString(context) + "\n" + " \n"); - } - if (!reference.equals("")) { - parseReference(reference); - } - if (!newContext.equals("")) { - writeString(" \n" - + " " + Utils.cleanString(newContext) - + "\n" + " \n"); - } - if (!flags.equals("")) { - writeString(" \n" + " " - + Utils.cleanString(flags).trim() + "\n" + " \n"); - } - // write singular first - if (!pluralTargets.isEmpty()) { - target = pluralTargets.get(0); - } else { - target = ""; - } - String approved = "no"; - if (!fuzzy && target.trim().length() > 0) { - approved = "yes"; - } - writeString(" \n"); - if (cformat) { - writeString(" " - + parseString(Utils.cleanString(source)) + "\n"); - if (target.length() > 0 || approved.equals("yes")) { - writeString(" " + parseString(Utils.cleanString(target)) + "\n"); - } - } else { - writeString(" " + Utils.cleanString(source) - + "\n"); - if (target.length() > 0 || approved.equals("yes")) { - writeString(" " + Utils.cleanString(target) + "\n"); - } - } - if (!comment.equals("")) { - writeString(" " + Utils.cleanString(comment) + "\n"); - } - writeString(" " + "Singular form" + "\n"); - writeString(" \n"); - // write plurals - for (int i = 1; i < plurals; i++) { - if (pluralTargets.size() > i) { - target = pluralTargets.get(i); - } else { - target = ""; - } - if (!fuzzy && target.trim().length() > 0) { - approved = "yes"; - } - writeString(" \n"); - if (cformat) { - writeString(" " - + parseString(Utils.cleanString(plural_source)) + "\n"); - if (target.length() > 0 || approved.equals("yes")) { - writeString(" " + parseString(Utils.cleanString(target)) + "\n"); - } - } else { - writeString(" " - + Utils.cleanString(plural_source) + "\n"); - if (target.length() > 0 || approved.equals("yes")) { - writeString(" " + Utils.cleanString(target) + "\n"); - } - } - writeString(" " + "Plural form: [" + i + "]" - + "\n"); - writeString(" \n"); - } - writeString(" \n"); - } else { - // only singular - String approved = "no"; - if (!fuzzy && target.trim().length() > 0) { - approved = "yes"; - } - String restype = ""; - if (source.trim().equals("")) { - restype = " restype=\"x-gettext-domain-header\" "; - } - writeString(" \n"); - if (cformat) { - writeString(" " - + parseString(Utils.cleanString(source)) + "\n"); - if (target.length() > 0 || approved.equals("yes")) { - writeString(" " + parseString(Utils.cleanString(target)) + "\n"); - } - } else { - if (source.trim().equals("")) { - source = target; - } - writeString(" " + Utils.cleanString(source) - + "\n"); - if (target.length() > 0 || approved.equals("yes")) { - writeString(" " + Utils.cleanString(target) + "\n"); - } - } - if (!comment.equals("")) { - writeString(" " + Utils.cleanString(comment) + "\n"); - } - if (!context.equals("")) { - writeString(" \n" + " " - + Utils.cleanString(context) + "\n" + " \n"); - } - if (!reference.equals("")) { - parseReference(reference); - } - if (!newContext.equals("")) { - writeString(" \n" - + " " + Utils.cleanString(newContext) - + "\n" + " \n"); - } - if (!flags.equals("")) { - writeString(" \n" + " " - + Utils.cleanString(flags).trim() + "\n" + " \n"); - } - writeString(" \n"); - } - - writeSkeleton("%%%" + segId++ + "%%%\n"); - - source = ""; - plural_source = ""; - pluralTargets.removeAllElements(); - target = ""; - comment = ""; - context = ""; - reference = ""; - flags = ""; - newContext = ""; - fuzzy = false; - cformat = false; - } - - private static String parseString(String string) { - // Valid c format especifications must end - // with one of diouxXfeEgGcs - - int id = 1; - int index = string.indexOf('%'); - if (index == -1) { - return string; - } - if (string.charAt(index + 1) == '%') { - index = string.indexOf('%', index + 2); - } - String result = ""; - while (index != -1) { - result = result + string.substring(0, index) + ""; - int i = index; - char c = string.charAt(i++); - while (i < string.length() && "diouxXfeEgGcs".indexOf(c) == -1) { - result = result + c; - c = string.charAt(i++); - } - result = result + c + ""; - string = string.substring(i); - index = string.indexOf('%'); - if (index != -1 && index < string.length() && string.charAt(index + 1) == '%') { - index = string.indexOf('%', index + 2); - } - } - result = result + string; - return result; - } - - private static void parseReference(String ref) throws IOException { - if (ref.trim().equals("")) { - return; - } - if (ref.indexOf(':') != -1) { - // regular reference - StringTokenizer tokenizer = new StringTokenizer(ref.trim()); - while (tokenizer.hasMoreTokens()) { - writeString(" \n"); - String token = tokenizer.nextToken(); - if (token.indexOf(':') != -1) { - writeString(" " - + token.substring(0, token.indexOf(':')) + "\n"); - writeString(" " - + token.substring(token.indexOf(':') + 1) + "\n"); - } - writeString(" \n"); - } - } else { - // strange thing, may be from .properties file - writeString(" \n"); - writeString( - " " + Utils.cleanString(ref).trim() + "\n"); - writeString(" \n"); - } - } - -} +/******************************************************************************* + * Copyright (c) 2003-2019 Maxprograms. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/org/documents/epl-v10.html + * + * Contributors: + * Maxprograms - initial API and implementation + *******************************************************************************/ + +package com.maxprograms.converters.po; + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Hashtable; +import java.util.StringTokenizer; +import java.util.Vector; +import java.lang.System.Logger.Level; +import java.lang.System.Logger; + +import com.maxprograms.converters.Constants; +import com.maxprograms.converters.Utils; + +public class Po2Xliff { + + private static FileOutputStream output; + private static FileOutputStream skeleton; + + private static String source; + private static String target; + private static String comment; + private static String context; + private static String reference; + private static String flags; + private static boolean fuzzy; + private static boolean cformat; + + private static String sourceLanguage; + private static int segId; + private static int domainId; + private static int contextId = 1; + private static int refId = 1; + private static String newContext; + private static Vector pluralTargets; + private static int plurals; + private static String plural_source; + + private Po2Xliff() { + // do not instantiate this class + // use run method instead + } + + public static Vector run(Hashtable params) { + Vector result = new Vector<>(); + + String inputFile = params.get("source"); + String xliffFile = params.get("xliff"); + String skeletonFile = params.get("skeleton"); + sourceLanguage = params.get("srcLang"); + String targetLanguage = params.get("tgtLang"); + String srcEncoding = params.get("srcEncoding"); + String tgtLang = ""; + if (targetLanguage != null) { + tgtLang = "\" target-language=\"" + targetLanguage; + } + + source = ""; + plural_source = ""; + pluralTargets = new Vector<>(); + target = ""; + comment = ""; + context = ""; + reference = ""; + flags = ""; + newContext = ""; + fuzzy = false; + boolean inDomain = false; + cformat = false; + + try { + try (FileInputStream stream = new FileInputStream(inputFile)) { + try (InputStreamReader input = new InputStreamReader(stream, srcEncoding)) { + BufferedReader buffer = new BufferedReader(input); + + output = new FileOutputStream(xliffFile); + + writeString("\n"); + writeString("\n"); + writeString("\n"); + + writeString("\n"); + writeString("
\n"); + writeString(" \n"); + writeString(" \n"); + writeString(" \n"); + writeString("
\n"); + writeString("\n"); + + skeleton = new FileOutputStream(skeletonFile); + + String line; + while ((line = buffer.readLine()) != null) { + line = line + "\n"; + + if (line.trim().length() == 0) { + // no text in this line + // segment separator + writeSkeleton(line); + } else { + if (line.startsWith("#:")) { + // it is a reference + if (reference.equals("")) { + reference = line.substring(2); + } else { + reference = reference + " " + line.substring(2); + } + } + if (line.startsWith("# ")) { + // translator comment + comment = comment + line.substring(2); + } + if (line.trim().equals("#")) { + comment = comment + "\n"; + } + if (line.startsWith("#.")) { + // automatic comment + context = context + line.substring(2); + } + if (line.startsWith("#,")) { + flags = line.substring(2); + // check for fuzzy + if (flags.indexOf("fuzzy") != -1) { + fuzzy = true; + } + // Only c-format is parsed. Tags from other + // formats, like php-format or python-format, + // are left as part of the text + if (flags.indexOf("c-format") != -1 && flags.indexOf("no-c-format") == -1) { + cformat = true; + } + } + if (line.startsWith("#~")) { + // commented entry + writeSkeleton(line); + } + if (line.startsWith("msgctxt")) { + // get context text + line = line.substring(5); + newContext = line.substring(line.indexOf('\"') + 1, line.lastIndexOf('\"')); + line = buffer.readLine(); + while (line.startsWith("\"")) { + newContext = newContext + "\n" + + line.substring(line.indexOf('\"') + 1, line.lastIndexOf('\"')); + line = buffer.readLine(); + } + continue; + } + if (line.startsWith("msgid")) { + if (line.startsWith("msgid_plural")) { + // get plural source + line = line.substring(12); + plural_source = line.substring(line.indexOf('\"') + 1, line.lastIndexOf('\"')); + line = buffer.readLine(); + while (line.startsWith("\"")) { + plural_source = plural_source + "\n" + + line.substring(line.indexOf('\"') + 1, line.lastIndexOf('\"')); + line = buffer.readLine(); + } + } else { + // get source text + line = line.substring(5); + source = line.substring(line.indexOf('\"') + 1, line.lastIndexOf('\"')); + line = buffer.readLine(); + while (line.trim().startsWith("\"")) { + source = source + "\n" + + line.substring(line.indexOf('\"') + 1, line.lastIndexOf('\"')); + line = buffer.readLine(); + } + } + continue; + } + if (line.startsWith("msgstr")) { + if (line.startsWith("msgstr[")) { + while (line.startsWith("msgstr[")) { + // get all plural targets + line = line.substring(line.indexOf(']') + 1); + String pluralTarget = line.substring(line.indexOf('\"') + 1, + line.lastIndexOf('\"')); + line = buffer.readLine(); + while (line != null && line.trim().startsWith("\"")) { + pluralTarget = pluralTarget + "\n" + + line.substring(line.indexOf('\"') + 1, line.lastIndexOf('\"')); + line = buffer.readLine(); + if (line == null) { + line = ""; + } + } + if (line == null) { + line = ""; + } + pluralTargets.add(pluralTarget); + } + writeSegment(); + } else { + // get the target + line = line.substring(6); + target = line.substring(line.indexOf('\"') + 1, line.lastIndexOf('\"')); + line = buffer.readLine(); + while (line != null && line.trim().startsWith("\"")) { + target = target + "\n" + + line.substring(line.indexOf('\"') + 1, line.lastIndexOf('\"')); + line = buffer.readLine(); + if (line == null) { + line = ""; + } + if (line.trim().startsWith("\"Plural-Forms:")) { + parsePlural(line); + } + } + if (plural_source.equals("")) { + writeSegment(); + } + } + continue; + } + if (line.startsWith("domain")) { + if (inDomain) { + writeString(" \n"); + } + inDomain = true; + writeString( + " \n"); + writeSkeleton(line); + } + } + } + + skeleton.close(); + + if (inDomain) { + writeString(" \n"); + } + writeString("\n"); + writeString("
\n"); + writeString("
"); + output.close(); + } + } + result.add(0, Constants.SUCCESS); + } catch (IOException e) { + Logger logger = System.getLogger(Po2Xliff.class.getName()); + logger.log(Level.ERROR, "Error converting PO file", e); + result.add(Constants.ERROR); + result.add(e.getMessage()); + } + + return result; + } + + private static void parsePlural(String line) { + String string = line.substring(line.indexOf("nplurals") + 8).trim(); + String number = string.substring(string.indexOf('=') + 1, string.indexOf(';')); + plurals = Integer.parseInt(number); + } + + private static void writeString(String string) throws IOException { + output.write(string.getBytes(StandardCharsets.UTF_8)); + } + + private static void writeSkeleton(String string) throws IOException { + skeleton.write(string.getBytes(StandardCharsets.UTF_8)); + } + + private static void writeSegment() throws IOException { + if (!plural_source.equals("")) { + writeString(" \n"); + if (!context.equals("")) { + writeString(" \n" + " " + + Utils.cleanString(context) + "\n" + " \n"); + } + if (!reference.equals("")) { + parseReference(reference); + } + if (!newContext.equals("")) { + writeString(" \n" + + " " + Utils.cleanString(newContext) + + "\n" + " \n"); + } + if (!flags.equals("")) { + writeString(" \n" + " " + + Utils.cleanString(flags).trim() + "\n" + " \n"); + } + // write singular first + if (!pluralTargets.isEmpty()) { + target = pluralTargets.get(0); + } else { + target = ""; + } + String approved = "no"; + if (!fuzzy && target.trim().length() > 0) { + approved = "yes"; + } + writeString(" \n"); + if (cformat) { + writeString(" " + + parseString(Utils.cleanString(source)) + "\n"); + if (target.length() > 0 || approved.equals("yes")) { + writeString(" " + parseString(Utils.cleanString(target)) + "\n"); + } + } else { + writeString(" " + Utils.cleanString(source) + + "\n"); + if (target.length() > 0 || approved.equals("yes")) { + writeString(" " + Utils.cleanString(target) + "\n"); + } + } + if (!comment.equals("")) { + writeString(" " + Utils.cleanString(comment) + "\n"); + } + writeString(" " + "Singular form" + "\n"); + writeString(" \n"); + // write plurals + for (int i = 1; i < plurals; i++) { + if (pluralTargets.size() > i) { + target = pluralTargets.get(i); + } else { + target = ""; + } + if (!fuzzy && target.trim().length() > 0) { + approved = "yes"; + } + writeString(" \n"); + if (cformat) { + writeString(" " + + parseString(Utils.cleanString(plural_source)) + "\n"); + if (target.length() > 0 || approved.equals("yes")) { + writeString(" " + parseString(Utils.cleanString(target)) + "\n"); + } + } else { + writeString(" " + + Utils.cleanString(plural_source) + "\n"); + if (target.length() > 0 || approved.equals("yes")) { + writeString(" " + Utils.cleanString(target) + "\n"); + } + } + writeString(" " + "Plural form: [" + i + "]" + + "\n"); + writeString(" \n"); + } + writeString(" \n"); + } else { + // only singular + String approved = "no"; + if (!fuzzy && target.trim().length() > 0) { + approved = "yes"; + } + String restype = ""; + if (source.trim().equals("")) { + restype = " restype=\"x-gettext-domain-header\" "; + } + writeString(" \n"); + if (cformat) { + writeString(" " + + parseString(Utils.cleanString(source)) + "\n"); + if (target.length() > 0 || approved.equals("yes")) { + writeString(" " + parseString(Utils.cleanString(target)) + "\n"); + } + } else { + if (source.trim().equals("")) { + source = target; + } + writeString(" " + Utils.cleanString(source) + + "\n"); + if (target.length() > 0 || approved.equals("yes")) { + writeString(" " + Utils.cleanString(target) + "\n"); + } + } + if (!comment.equals("")) { + writeString(" " + Utils.cleanString(comment) + "\n"); + } + if (!context.equals("")) { + writeString(" \n" + " " + + Utils.cleanString(context) + "\n" + " \n"); + } + if (!reference.equals("")) { + parseReference(reference); + } + if (!newContext.equals("")) { + writeString(" \n" + + " " + Utils.cleanString(newContext) + + "\n" + " \n"); + } + if (!flags.equals("")) { + writeString(" \n" + " " + + Utils.cleanString(flags).trim() + "\n" + " \n"); + } + writeString(" \n"); + } + + writeSkeleton("%%%" + segId++ + "%%%\n"); + + source = ""; + plural_source = ""; + pluralTargets.removeAllElements(); + target = ""; + comment = ""; + context = ""; + reference = ""; + flags = ""; + newContext = ""; + fuzzy = false; + cformat = false; + } + + private static String parseString(String string) { + // Valid c format especifications must end + // with one of diouxXfeEgGcs + + int id = 1; + int index = string.indexOf('%'); + if (index == -1) { + return string; + } + if (string.charAt(index + 1) == '%') { + index = string.indexOf('%', index + 2); + } + String result = ""; + while (index != -1) { + result = result + string.substring(0, index) + ""; + int i = index; + char c = string.charAt(i++); + while (i < string.length() && "diouxXfeEgGcs".indexOf(c) == -1) { + result = result + c; + c = string.charAt(i++); + } + result = result + c + ""; + string = string.substring(i); + index = string.indexOf('%'); + if (index != -1 && index < string.length() && string.charAt(index + 1) == '%') { + index = string.indexOf('%', index + 2); + } + } + result = result + string; + return result; + } + + private static void parseReference(String ref) throws IOException { + if (ref.trim().equals("")) { + return; + } + if (ref.indexOf(':') != -1) { + // regular reference + StringTokenizer tokenizer = new StringTokenizer(ref.trim()); + while (tokenizer.hasMoreTokens()) { + writeString(" \n"); + String token = tokenizer.nextToken(); + if (token.indexOf(':') != -1) { + writeString(" " + + token.substring(0, token.indexOf(':')) + "\n"); + writeString(" " + + token.substring(token.indexOf(':') + 1) + "\n"); + } + writeString(" \n"); + } + } else { + // strange thing, may be from .properties file + writeString(" \n"); + writeString( + " " + Utils.cleanString(ref).trim() + "\n"); + writeString(" \n"); + } + } + +} diff --git a/src/com/maxprograms/converters/po/Xliff2Po.java b/src/com/maxprograms/converters/po/Xliff2Po.java index cd3fa5cd..ba3d2fba 100644 --- a/src/com/maxprograms/converters/po/Xliff2Po.java +++ b/src/com/maxprograms/converters/po/Xliff2Po.java @@ -1,376 +1,377 @@ -/******************************************************************************* - * Copyright (c) 2003-2019 Maxprograms. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 1.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/org/documents/epl-v10.html - * - * Contributors: - * Maxprograms - initial API and implementation - *******************************************************************************/ -package com.maxprograms.converters.po; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.text.MessageFormat; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.List; -import java.util.StringTokenizer; -import java.util.Vector; -import java.lang.System.Logger.Level; -import java.lang.System.Logger; - -import javax.xml.parsers.ParserConfigurationException; - -import org.xml.sax.SAXException; - -import com.maxprograms.converters.UnexistentSegmentException; -import com.maxprograms.xml.Document; -import com.maxprograms.xml.Element; -import com.maxprograms.xml.SAXBuilder; - -public class Xliff2Po { - - private static String xliffFile; - private static Hashtable segments; - private static FileOutputStream output; - private static String encoding; - - private Xliff2Po() { - // do not instantiate this class - // use run method instead - } - - public static Vector run(Hashtable params) { - - Vector result = new Vector<>(); - - String sklFile = params.get("skeleton"); - xliffFile = params.get("xliff"); - encoding = params.get("encoding"); - - try { - String outputFile = params.get("backfile"); - File f = new File(outputFile); - File p = f.getParentFile(); - if (p == null) { - p = new File(System.getProperty("user.dir")); - } - if (!p.exists()) { - p.mkdirs(); - } - if (!f.exists()) { - Files.createFile(Paths.get(f.toURI())); - } - output = new FileOutputStream(f); - loadSegments(); - try (InputStreamReader input = new InputStreamReader(new FileInputStream(sklFile), - StandardCharsets.UTF_8)) { - BufferedReader buffer = new BufferedReader(input); - String line; - while ((line = buffer.readLine()) != null) { - line = line + "\n"; - - if (line.indexOf("%%%") != -1) { - // - // contains translatable text - // - int index = line.indexOf("%%%"); - while (index != -1) { - String start = line.substring(0, index); - writeString(start); - line = line.substring(index + 3); - String code = line.substring(0, line.indexOf("%%%")); - line = line.substring(line.indexOf("%%%") + 3); - Element segment = segments.get(code); - if (segment != null) { - writeSegment(segment); - } else { - MessageFormat mf = new MessageFormat("Segment {0} not found."); - throw new UnexistentSegmentException(mf.format(new Object[] { code })); - } - - index = line.indexOf("%%%"); - if (index == -1) { - writeString(line); - } - } // end while - } else { - // - // non translatable portion - // - writeString(line); - } - } - } - output.close(); - result.add("0"); - } catch (IOException | SAXException | UnexistentSegmentException | ParserConfigurationException e) { - Logger logger = System.getLogger(Xliff2Po.class.getName()); - logger.log(Level.ERROR, "Error merging PO file.", e); - result.add("1"); - result.add(e.getMessage()); - } - return result; - } - - private static void writeSegment(Element segment) throws IOException { - if (segment.getName().equals("trans-unit")) { - // singular only - Element target = segment.getChild("target"); - Element source = segment.getChild("source"); - boolean newLine = false; - boolean fuzzy = false; - if (!segment.getAttributeValue("approved", "no").equalsIgnoreCase("Yes") && target != null - && !target.getText().trim().equals("")) { - fuzzy = true; - } - writeComments(segment); - writeContext(segment); - writeReferences(segment); - writeFlags(segment, fuzzy); - - if (source.getText().endsWith("\n")) { - newLine = true; - } else { - newLine = false; - } - if (!segment.getAttributeValue("restype", "").equals("x-gettext-domain-header")) { - writeString("msgid \"" + addQuotes(source.getText()) + "\"\n"); - } else { - writeString("msgid \"\"\n"); - } - if (target != null) { - String text = target.getText(); - if (newLine && !text.endsWith("\n")) { - text = text + "\"\"\n"; - } - writeString("msgstr \"" + addQuotes(text) + "\""); - } else { - writeString("msgstr \"\""); - } - } else { - // has plurals - writeComments(segment); - writeContext(segment); - writeReferences(segment); - List units = segment.getChildren("trans-unit"); - boolean fuzzy = false; - for (int i = 0; i < units.size(); i++) { - if (units.get(i).getAttributeValue("approved").equalsIgnoreCase("no")) { - fuzzy = true; - } - } - writeFlags(segment, fuzzy); - Element singular = units.get(0); - Element source = singular.getChild("source"); - writeString("msgid \"" + addQuotes(source.getText()) + "\"\n"); - if (units.size() > 1) { - Element plural = units.get(1); - source = plural.getChild("source"); - writeString("msgid_plural \"" + addQuotes(source.getText()) + "\"\n"); - for (int i = 0; i < units.size(); i++) { - Element target = units.get(i).getChild("target"); - if (target != null) { - writeString("msgstr[" + i + "] \"" + addQuotes(target.getText()) + "\"\n"); - } else { - writeString("msgstr[" + i + "] \"\"\n"); - } - } - } - } - } - - private static void writeFlags(Element segment, boolean fuzzy) throws IOException { - List groups = segment.getChildren("prop-group"); - Iterator i = groups.iterator(); - String flags = ""; - while (i.hasNext()) { - Element group = i.next(); - List contexts = group.getChildren(); - Iterator h = contexts.iterator(); - while (h.hasNext()) { - Element prop = h.next(); - if (prop.getAttributeValue("ctype", "").equals("x-po-flags")) { - flags = prop.getText(); - } - } - } - if (fuzzy) { - if (flags.indexOf("fuzzy") == -1) { - writeString("#, fuzzy " + flags + "\n"); - } else { - writeString("#, " + flags + "\n"); - } - } else { - if (flags.indexOf("fuzzy") == -1) { - if (!flags.equals("")) { - writeString("#, " + flags + "\n"); - } - } else { - flags = flags.substring(0, flags.indexOf("fuzzy")) + flags.substring(flags.indexOf("fuzzy") + 5); - if (!flags.equals("")) { - writeString("#, " + flags + "\n"); - } - } - } - } - - private static void writeReferences(Element segment) throws IOException { - String reference = "#:"; - String newContext = "msgctxt \""; - List groups = segment.getChildren("context-group"); - Iterator i = groups.iterator(); - while (i.hasNext()) { - Element group = i.next(); - if (group.getAttributeValue("name", "").startsWith("x-po-reference") - && group.getAttributeValue("purpose").equals("location")) { - String file = ""; - String linenumber = ""; - List contexts = group.getChildren(); - Iterator h = contexts.iterator(); - while (h.hasNext()) { - Element context = h.next(); - if (context.getAttributeValue("context-type", "").equals("sourcefile")) { - file = context.getText(); - } - if (context.getAttributeValue("context-type", "").equals("linenumber")) { - linenumber = context.getText(); - } - } - String test = reference + " " + file + ":" + linenumber; - if (test.substring(test.lastIndexOf("#:")).length() > 80) { - reference = reference + "\n#:"; - } - reference = reference + " " + file + ":" + linenumber; - } - if (group.getAttributeValue("name", "").startsWith("x-po-reference") - && group.getAttributeValue("purpose").equals("x-unknown")) { - List contexts = group.getChildren(); - Iterator h = contexts.iterator(); - while (h.hasNext()) { - Element context = h.next(); - reference = reference + " " + context.getText(); - } - } - if (group.getAttributeValue("name", "").startsWith("x-po-msgctxt")) { - List contexts = group.getChildren(); - Iterator h = contexts.iterator(); - while (h.hasNext()) { - Element context = h.next(); - newContext = newContext + context.getText(); - } - } - } - if (!reference.equals("#:")) { - writeString(reference + "\n"); - } - if (!newContext.equals("msgctxt \"")) { - writeString(newContext + "\"\n"); - } - } - - private static void writeContext(Element segment) throws IOException { - List groups = segment.getChildren("context-group"); - Iterator i = groups.iterator(); - while (i.hasNext()) { - Element group = i.next(); - if (group.getAttributeValue("name", "").startsWith("x-po-entry-header") - && group.getAttributeValue("purpose").equals("information")) { - List contexts = group.getChildren(); - Iterator h = contexts.iterator(); - while (h.hasNext()) { - Element context = h.next(); - if (context.getAttributeValue("context-type", "").equals("x-po-autocomment")) { - Vector comments = splitLines(context.getText()); - for (int j = 0; j < comments.size(); j++) { - String comment = comments.get(j); - if (!comment.trim().equals("")) { - writeString("#. " + comment.trim() + "\n"); - } else { - writeString("#.\n"); - } - } - } - } - } - } - } - - private static void writeComments(Element segment) throws IOException { - List notes = segment.getChildren("note"); - Iterator i = notes.iterator(); - while (i.hasNext()) { - Element note = i.next(); - if (note.getAttributeValue("annotates", "general").equals("source")) { - continue; - } - Vector lines = splitLines(note.getText()); - Iterator h = lines.iterator(); - while (h.hasNext()) { - String comment = h.next(); - writeString("# " + comment.trim() + "\n"); - } - } - } - - private static Vector splitLines(String text) { - Vector result = new Vector<>(); - StringTokenizer tokenizer = new StringTokenizer(text, "\n"); - if (text.startsWith("\n\n")) { - result.add(""); - } - while (tokenizer.hasMoreTokens()) { - result.add(tokenizer.nextToken()); - } - if (text.endsWith("\n\n")) { - result.add(""); - } - return result; - } - - private static String addQuotes(String string) { - return string.replaceAll("\n", "\"\n\""); - } - - private static void loadSegments() throws SAXException, IOException, ParserConfigurationException { - - SAXBuilder builder = new SAXBuilder(); - - Document doc = builder.build(xliffFile); - Element root = doc.getRootElement(); - segments = new Hashtable<>(); - - recurse(root); - - } - - private static void recurse(Element e) { - List list = e.getChildren(); - Iterator i = list.iterator(); - while (i.hasNext()) { - Element u = i.next(); - if (u.getName().equals("trans-unit")) { - segments.put(u.getAttributeValue("id"), u); - } else if (u.getName().equals("group") && u.getAttributeValue("restype", "").equals("x-gettext-plurals")) { - segments.put(u.getAttributeValue("id"), u); - } else { - recurse(u); - } - } - } - - private static void writeString(String string) throws IOException { - output.write(string.getBytes(encoding)); - } - -} +/******************************************************************************* + * Copyright (c) 2003-2019 Maxprograms. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/org/documents/epl-v10.html + * + * Contributors: + * Maxprograms - initial API and implementation + *******************************************************************************/ +package com.maxprograms.converters.po; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.text.MessageFormat; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.StringTokenizer; +import java.util.Vector; +import java.lang.System.Logger.Level; +import java.lang.System.Logger; + +import javax.xml.parsers.ParserConfigurationException; + +import org.xml.sax.SAXException; + +import com.maxprograms.converters.Constants; +import com.maxprograms.converters.UnexistentSegmentException; +import com.maxprograms.xml.Document; +import com.maxprograms.xml.Element; +import com.maxprograms.xml.SAXBuilder; + +public class Xliff2Po { + + private static String xliffFile; + private static Hashtable segments; + private static FileOutputStream output; + private static String encoding; + + private Xliff2Po() { + // do not instantiate this class + // use run method instead + } + + public static Vector run(Hashtable params) { + + Vector result = new Vector<>(); + + String sklFile = params.get("skeleton"); + xliffFile = params.get("xliff"); + encoding = params.get("encoding"); + + try { + String outputFile = params.get("backfile"); + File f = new File(outputFile); + File p = f.getParentFile(); + if (p == null) { + p = new File(System.getProperty("user.dir")); + } + if (!p.exists()) { + p.mkdirs(); + } + if (!f.exists()) { + Files.createFile(Paths.get(f.toURI())); + } + output = new FileOutputStream(f); + loadSegments(); + try (InputStreamReader input = new InputStreamReader(new FileInputStream(sklFile), + StandardCharsets.UTF_8)) { + BufferedReader buffer = new BufferedReader(input); + String line; + while ((line = buffer.readLine()) != null) { + line = line + "\n"; + + if (line.indexOf("%%%") != -1) { + // + // contains translatable text + // + int index = line.indexOf("%%%"); + while (index != -1) { + String start = line.substring(0, index); + writeString(start); + line = line.substring(index + 3); + String code = line.substring(0, line.indexOf("%%%")); + line = line.substring(line.indexOf("%%%") + 3); + Element segment = segments.get(code); + if (segment != null) { + writeSegment(segment); + } else { + MessageFormat mf = new MessageFormat("Segment {0} not found."); + throw new UnexistentSegmentException(mf.format(new Object[] { code })); + } + + index = line.indexOf("%%%"); + if (index == -1) { + writeString(line); + } + } // end while + } else { + // + // non translatable portion + // + writeString(line); + } + } + } + output.close(); + result.add(Constants.SUCCESS); + } catch (IOException | SAXException | UnexistentSegmentException | ParserConfigurationException e) { + Logger logger = System.getLogger(Xliff2Po.class.getName()); + logger.log(Level.ERROR, "Error merging PO file.", e); + result.add(Constants.ERROR); + result.add(e.getMessage()); + } + return result; + } + + private static void writeSegment(Element segment) throws IOException { + if (segment.getName().equals("trans-unit")) { + // singular only + Element target = segment.getChild("target"); + Element source = segment.getChild("source"); + boolean newLine = false; + boolean fuzzy = false; + if (!segment.getAttributeValue("approved", "no").equalsIgnoreCase("Yes") && target != null + && !target.getText().trim().equals("")) { + fuzzy = true; + } + writeComments(segment); + writeContext(segment); + writeReferences(segment); + writeFlags(segment, fuzzy); + + if (source.getText().endsWith("\n")) { + newLine = true; + } else { + newLine = false; + } + if (!segment.getAttributeValue("restype", "").equals("x-gettext-domain-header")) { + writeString("msgid \"" + addQuotes(source.getText()) + "\"\n"); + } else { + writeString("msgid \"\"\n"); + } + if (target != null) { + String text = target.getText(); + if (newLine && !text.endsWith("\n")) { + text = text + "\"\"\n"; + } + writeString("msgstr \"" + addQuotes(text) + "\""); + } else { + writeString("msgstr \"\""); + } + } else { + // has plurals + writeComments(segment); + writeContext(segment); + writeReferences(segment); + List units = segment.getChildren("trans-unit"); + boolean fuzzy = false; + for (int i = 0; i < units.size(); i++) { + if (units.get(i).getAttributeValue("approved").equalsIgnoreCase("no")) { + fuzzy = true; + } + } + writeFlags(segment, fuzzy); + Element singular = units.get(0); + Element source = singular.getChild("source"); + writeString("msgid \"" + addQuotes(source.getText()) + "\"\n"); + if (units.size() > 1) { + Element plural = units.get(1); + source = plural.getChild("source"); + writeString("msgid_plural \"" + addQuotes(source.getText()) + "\"\n"); + for (int i = 0; i < units.size(); i++) { + Element target = units.get(i).getChild("target"); + if (target != null) { + writeString("msgstr[" + i + "] \"" + addQuotes(target.getText()) + "\"\n"); + } else { + writeString("msgstr[" + i + "] \"\"\n"); + } + } + } + } + } + + private static void writeFlags(Element segment, boolean fuzzy) throws IOException { + List groups = segment.getChildren("prop-group"); + Iterator i = groups.iterator(); + String flags = ""; + while (i.hasNext()) { + Element group = i.next(); + List contexts = group.getChildren(); + Iterator h = contexts.iterator(); + while (h.hasNext()) { + Element prop = h.next(); + if (prop.getAttributeValue("ctype", "").equals("x-po-flags")) { + flags = prop.getText(); + } + } + } + if (fuzzy) { + if (flags.indexOf("fuzzy") == -1) { + writeString("#, fuzzy " + flags + "\n"); + } else { + writeString("#, " + flags + "\n"); + } + } else { + if (flags.indexOf("fuzzy") == -1) { + if (!flags.equals("")) { + writeString("#, " + flags + "\n"); + } + } else { + flags = flags.substring(0, flags.indexOf("fuzzy")) + flags.substring(flags.indexOf("fuzzy") + 5); + if (!flags.equals("")) { + writeString("#, " + flags + "\n"); + } + } + } + } + + private static void writeReferences(Element segment) throws IOException { + String reference = "#:"; + String newContext = "msgctxt \""; + List groups = segment.getChildren("context-group"); + Iterator i = groups.iterator(); + while (i.hasNext()) { + Element group = i.next(); + if (group.getAttributeValue("name", "").startsWith("x-po-reference") + && group.getAttributeValue("purpose").equals("location")) { + String file = ""; + String linenumber = ""; + List contexts = group.getChildren(); + Iterator h = contexts.iterator(); + while (h.hasNext()) { + Element context = h.next(); + if (context.getAttributeValue("context-type", "").equals("sourcefile")) { + file = context.getText(); + } + if (context.getAttributeValue("context-type", "").equals("linenumber")) { + linenumber = context.getText(); + } + } + String test = reference + " " + file + ":" + linenumber; + if (test.substring(test.lastIndexOf("#:")).length() > 80) { + reference = reference + "\n#:"; + } + reference = reference + " " + file + ":" + linenumber; + } + if (group.getAttributeValue("name", "").startsWith("x-po-reference") + && group.getAttributeValue("purpose").equals("x-unknown")) { + List contexts = group.getChildren(); + Iterator h = contexts.iterator(); + while (h.hasNext()) { + Element context = h.next(); + reference = reference + " " + context.getText(); + } + } + if (group.getAttributeValue("name", "").startsWith("x-po-msgctxt")) { + List contexts = group.getChildren(); + Iterator h = contexts.iterator(); + while (h.hasNext()) { + Element context = h.next(); + newContext = newContext + context.getText(); + } + } + } + if (!reference.equals("#:")) { + writeString(reference + "\n"); + } + if (!newContext.equals("msgctxt \"")) { + writeString(newContext + "\"\n"); + } + } + + private static void writeContext(Element segment) throws IOException { + List groups = segment.getChildren("context-group"); + Iterator i = groups.iterator(); + while (i.hasNext()) { + Element group = i.next(); + if (group.getAttributeValue("name", "").startsWith("x-po-entry-header") + && group.getAttributeValue("purpose").equals("information")) { + List contexts = group.getChildren(); + Iterator h = contexts.iterator(); + while (h.hasNext()) { + Element context = h.next(); + if (context.getAttributeValue("context-type", "").equals("x-po-autocomment")) { + Vector comments = splitLines(context.getText()); + for (int j = 0; j < comments.size(); j++) { + String comment = comments.get(j); + if (!comment.trim().equals("")) { + writeString("#. " + comment.trim() + "\n"); + } else { + writeString("#.\n"); + } + } + } + } + } + } + } + + private static void writeComments(Element segment) throws IOException { + List notes = segment.getChildren("note"); + Iterator i = notes.iterator(); + while (i.hasNext()) { + Element note = i.next(); + if (note.getAttributeValue("annotates", "general").equals("source")) { + continue; + } + Vector lines = splitLines(note.getText()); + Iterator h = lines.iterator(); + while (h.hasNext()) { + String comment = h.next(); + writeString("# " + comment.trim() + "\n"); + } + } + } + + private static Vector splitLines(String text) { + Vector result = new Vector<>(); + StringTokenizer tokenizer = new StringTokenizer(text, "\n"); + if (text.startsWith("\n\n")) { + result.add(""); + } + while (tokenizer.hasMoreTokens()) { + result.add(tokenizer.nextToken()); + } + if (text.endsWith("\n\n")) { + result.add(""); + } + return result; + } + + private static String addQuotes(String string) { + return string.replaceAll("\n", "\"\n\""); + } + + private static void loadSegments() throws SAXException, IOException, ParserConfigurationException { + + SAXBuilder builder = new SAXBuilder(); + + Document doc = builder.build(xliffFile); + Element root = doc.getRootElement(); + segments = new Hashtable<>(); + + recurse(root); + + } + + private static void recurse(Element e) { + List list = e.getChildren(); + Iterator i = list.iterator(); + while (i.hasNext()) { + Element u = i.next(); + if (u.getName().equals("trans-unit")) { + segments.put(u.getAttributeValue("id"), u); + } else if (u.getName().equals("group") && u.getAttributeValue("restype", "").equals("x-gettext-plurals")) { + segments.put(u.getAttributeValue("id"), u); + } else { + recurse(u); + } + } + } + + private static void writeString(String string) throws IOException { + output.write(string.getBytes(encoding)); + } + +} diff --git a/src/com/maxprograms/converters/rc/Rc2Xliff.java b/src/com/maxprograms/converters/rc/Rc2Xliff.java index 00101ffc..f4a66f1c 100644 --- a/src/com/maxprograms/converters/rc/Rc2Xliff.java +++ b/src/com/maxprograms/converters/rc/Rc2Xliff.java @@ -1,674 +1,675 @@ -/******************************************************************************* - * Copyright (c) 2003-2019 Maxprograms. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 1.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/org/documents/epl-v10.html - * - * Contributors: - * Maxprograms - initial API and implementation - *******************************************************************************/ -/** - * - */ -package com.maxprograms.converters.rc; - -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.util.Hashtable; -import java.util.Vector; -import java.lang.System.Logger.Level; -import java.lang.System.Logger; - -import com.maxprograms.converters.Utils; - -public class Rc2Xliff { - - private static InputStreamReader buffer; - private static FileOutputStream output; - private static FileOutputStream skeleton; - private static String lastWord = ""; - private static String sourceLanguage; - private static int segId; - private static String stack; - private static int blockStack; - - private Rc2Xliff() { - // do not instantiate this class - // use run method instead - } - - public static Vector run(Hashtable params) { - Vector result = new Vector<>(); - segId = 0; - String inputFile = params.get("source"); - String xliffFile = params.get("xliff"); - String skeletonFile = params.get("skeleton"); - sourceLanguage = params.get("srcLang"); - String targetLanguage = params.get("tgtLang"); - String srcEncoding = params.get("srcEncoding"); - String tgtLang = ""; - if (targetLanguage != null) { - tgtLang = "\" target-language=\"" + targetLanguage; - } - - try { - try (FileInputStream input = new FileInputStream(inputFile)) { - buffer = new InputStreamReader(input, srcEncoding); - output = new FileOutputStream(xliffFile); - stack = ""; - writeString("\n"); - writeString("\n"); - writeString("\n"); - - writeString("\n"); - writeString("
\n"); - writeString(" \n"); - writeString(" \n"); - writeString(" \n"); - writeString("
\n"); - writeString("\n"); - - skeleton = new FileOutputStream(skeletonFile); - - parseRC(); - - skeleton.close(); - - writeString("\n"); - writeString("
\n"); - writeString("
"); - buffer.close(); - } - output.close(); - result.add("0"); // success - } catch (IOException e) { - Logger logger = System.getLogger(Rc2Xliff.class.getName()); - logger.log(Level.ERROR, "Error convering RC file", e); - result.add("1"); - result.add(e.getMessage()); - } - return result; - } - - private static void writeString(String string) throws IOException { - output.write(string.getBytes(StandardCharsets.UTF_8)); - } - - private static void writeSkeleton(String string) throws IOException { - skeleton.write(string.getBytes(StandardCharsets.UTF_8)); - } - - private static void writeSkeleton(char character) throws IOException { - writeSkeleton(String.valueOf(character)); - } - - private static void writeSegment(String segment) throws IOException { - if (segment.equals("")) { - return; - } - writeString(" \n" + " " + Utils.cleanString(segment) + "\n" + " \n"); - writeSkeleton("%%%" + segId++ + "%%%"); - } - - private static void parseRC() throws IOException { - char character; - while (buffer.ready()) { - character = (char) buffer.read(); - if (character == '#') {// directives - parseDirective(); - } else if (character == '/') {// comments - // comment /* or // - parseComment(); // Keep the state - } else if (!blankChar(character)) { - parseStatement(character); - } else { - writeSkeleton(character); - } - } - } - - private static void parseComment() throws IOException { - writeSkeleton("/"); // Last character read - - boolean bLargeComment; - char prevChar = ' '; - char character; - - if (buffer.ready()) { - character = (char) buffer.read(); - writeSkeleton(character); - bLargeComment = character == '*'; // Large comment /* */ - - // Add the comment to the skeleton - while (buffer.ready()) { - character = (char) buffer.read(); - writeSkeleton(character); - if (bLargeComment && prevChar == '*' && character == '/') { - break; - } else if (!bLargeComment && (character == '\n' || character == '\r')) { - break; - } - prevChar = character; - } - } - } - - private static boolean blankChar(char c) { - return c == ' ' || c == '\n' || c == '\t' || c == '\r'; - } - - private static boolean beginBlock(String word) { - return word.trim().equals("BEGIN") || word.trim().equals("{"); - } - - private static boolean endBlock(String word) { - return word.trim().equals("END") || word.trim().equals("}"); - } - - private static void parseStatement(char initial) throws IOException { - String statement = String.valueOf(initial); - writeSkeleton(initial); - statement = statement.concat(parseWords(" ,\n\r\t", true, false)); - if (statement.trim().equals("STRINGTABLE")) { - parseStringTable(); - } else if (statement.trim().equals("DIALOG") || statement.trim().equals("DIALOGEX")) { - parseDialog(); - } else if (statement.trim().equals("MENU") || statement.trim().equals("MENUEX")) { - parseMenu(); - } else if (statement.trim().equals("POPUP")) { - parsePopup(); - } else if (statement.trim().equals("DLGINIT")) { - parseDlgInit(); - } else if (beginBlock(statement.trim())) {// BEGIN - blockStack = 0; - parseBlock(); - } - } - - private static void parseDirective() throws IOException { - char character = ' '; - writeSkeleton('#'); - String statement = parseWords(" \t", true, false); - if (statement.trim().equals("define")) { - parseDefine(); - } else { - while (buffer.ready() && character != '\r' && character != '\n') { - character = (char) buffer.read(); - writeSkeleton(character); - } - } - } - - private static void parseDefine() throws IOException { - String word = ""; - while (buffer.ready()) { - stack = ""; - word = parseWords(" \n\t\r,\"L", false, true); - if (word.trim().equals("\"")) { - captureString(true); - } else { // is END or ID - writeSkeleton(stack); - if (word.equals("\r") || word.equals("\n")) { - break; - } - } - } - } - - private static void parseBlock() throws IOException { - blockStack++; - String statement = ""; - while (blockStack != 0 && buffer.ready()) { - statement = parseWords(" \n\t\r", true, false); - if (beginBlock(statement.trim())) { - blockStack++; - } else if (endBlock(statement.trim())) { - blockStack--; - } - } - } - - private static void parseDialog() throws IOException { - parseDialogContent(); - parseControlBlock(); - } - - private static void parseDialogContent() throws IOException { - String word = " "; - while (!beginBlock(word)) { - word = parseWords(" \n\t\r(),", true, false); - if (word.trim().equals("CAPTION")) { - captureString(false); - } - } - } - - private static void parseControlBlock() throws IOException { - boolean isEnd = false; - parseWords(" (),\r\n\t", true, false); - do { - lastWord = lastWord.trim(); - if (lastWord.equals("CONTROL") || lastWord.equals("LTEXT") || lastWord.equals("CTEXT") - || lastWord.equals("RTEXT") || lastWord.equals("AUTO3STATE") || //$NON-NLS-2$ - lastWord.equals("AUTOCHECKBOX") || lastWord.equals("AUTORADIOBUTTON") || lastWord.equals("CHECKBOX") - || lastWord.equals("PUSHBOX") || lastWord.equals("PUSHBUTTON") || lastWord.equals("DEFPUSHBUTTON") - || lastWord.equals("RADIOBUTTON") || lastWord.equals("STATE3") || lastWord.equals("USERBUTTON") - || lastWord.equals("GROUPBOX")) { - isEnd = parseControlTypeI(); - } else if (lastWord.equals("EDITTEXT") || lastWord.equals("BEDIT") || lastWord.equals("IEDIT") - || lastWord.equals("HEDIT") || lastWord.equals("COMBOBOX") || lastWord.equals("LISTBOX") - || lastWord.equals("SCROLLBAR") || lastWord.equals("ICON")) { - isEnd = parseControlTypeI(); - } else { - parseWords(" (),\r\t\n", true, false); - } - if (isEnd) {// End of block in last control - break; - } - } while (true); - } - - private static boolean isEndControlStatement(String word) { - // list of all posible controls and END keyword - String[] controls = new String[] { "END", "CONTROL", "LTEXT", "CTEXT", "RTEXT", "AUTO3STATE", "AUTOCHECKBOX", //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ - "AUTORADIOBUTTON", "CHECKBOX", "PUSHBOX", "PUSHBUTTON", "DEFPUSHBUTTON", //$NON-NLS-5$ - "RADIOBUTTON", "STATE3", "USERBUTTON", "GROUPBOX", "EDITTEXT", "BEDIT", //$NON-NLS-5$ //$NON-NLS-6$ - "IEDIT", "HEDIT", "COMBOBOX", "LISTBOX", "SCROLLBAR", "ICON" }; //$NON-NLS-5$ //$NON-NLS-6$ - - for (int i = 0; i < controls.length; i++) { - if (controls[i].equals(word)) { - return true; - } - } - return false; - } - - // return true if it has a block in the control - private static boolean parseControlTypeI() throws IOException { - char cIni = ' '; - while (blankChar(cIni) && buffer.ready()) { - cIni = (char) buffer.read(); - if (cIni != '"' && cIni != 'L') { - writeSkeleton(cIni); - } - } - - // first parameter optional string - if (cIni == '"') { - captureString(true); - } else { - if (cIni == 'L') { // String type L"String" - writeSkeleton(cIni); - cIni = (char) buffer.read(); - if (cIni == '"') { - captureString(true); - } else {// don't have string - writeSkeleton(cIni); - } - } - } - - String word = " "; - boolean hasBlock = false; - while (!isEndControlStatement(word)) { - word = parseWords(" \t(),\r\n", true, false).trim(); - if (beginBlock(word)) { // begin an optional data block in the control - hasBlock = true; - } - } - return !hasBlock && endBlock(word);// end of control block? - } - - private static void writeConditional(char character, boolean write) throws IOException { - if (write) { - writeSkeleton(character); - } else { - stack += String.valueOf(character); - } - - } - - private static void parseComment(boolean write, boolean large) throws IOException { - writeConditional('/', write); // Last character read - if (large) { - writeConditional('*', write); - } else { - writeConditional('/', write); - } - - char prevChar = ' '; - char character; - - // Add the comment to the skeleton - while (buffer.ready()) { - character = (char) buffer.read(); - writeConditional(character, write); - if (large && prevChar == '*' && character == '/') { - break; - } else if (!large && (character == '\n' || character == '\r')) { - break; - } - prevChar = character; - } - - } - - private static String parseWords(String separators, boolean write, boolean withSeparator) throws IOException { - String word = ""; - char lastChar; - char character = 'a'; // initial value any character not in separators - while (buffer.ready() && separators.indexOf(character) == -1) { - character = (char) buffer.read(); - if (character == '/') { - lastChar = character; - character = (char) buffer.read(); - if (character == '/') { - parseComment(write, false); - break; - } else if (character == '*') { // skip comments - parseComment(write, true); - break; - } else { // write the last two characters - if (write) { - writeSkeleton(lastChar); - writeSkeleton(character); - } else { - stack += String.valueOf(lastChar); - stack += String.valueOf(character); - } - word = word.concat(String.valueOf(lastChar)); - if (separators.indexOf(character) == -1 || withSeparator) { // return with the separator or not - word = word.concat(String.valueOf(character)); - } - } - } else if (character == '\\') { - lastChar = character; - character = (char) buffer.read(); - if (character != '\r' || character != '\n') { - if (write) { // must write and \character - writeSkeleton(lastChar); - writeSkeleton(character); - while (blankChar(character) && character != ' ') { - character = (char) buffer.read(); - writeSkeleton(character); - } - word = word.concat(String.valueOf(character)); - } else {// not write and \ character - stack += String.valueOf(lastChar); - stack += String.valueOf(character); - while (blankChar(character)) { - character = (char) buffer.read(); - stack += String.valueOf(character); - } - word = word.concat(String.valueOf(character)); - } - } else { - if (write) { - writeSkeleton(lastChar); - writeSkeleton(character); - } else { - stack += String.valueOf(lastChar); - stack += String.valueOf(character); - } - word = word.concat(String.valueOf(lastChar)); - if (separators.indexOf(character) == -1 || withSeparator) { // return with the separator or not - word = word.concat(String.valueOf(character)); - } - } - } else { // not is a special character (comment or \) - - if (write) { - writeSkeleton(character); - } else { - - stack += String.valueOf(character); - } - if (separators.indexOf(character) == -1 || withSeparator) { // return with the separator or not - word = word.concat(String.valueOf(character)); - } - } - } - lastWord = word; - return word; - } - - private static void captureString(boolean startNow) throws IOException { - int quotes = 0; - if (startNow) { - quotes = 1; // now in the string - } - char character; - char lastChar; - String word = ""; - while (buffer.ready() && quotes < 2) { - character = (char) buffer.read(); - if (quotes == 0) { // not in the string yet - if (character == '"') {// begining of string - quotes++; - } else {// not in the string yet - writeSkeleton(character); - } - } else { // is in the string - if (character == '"') { // end of string Careful can be the \" escape character - if (word.equals("")) { - writeSkeleton('"'); - writeSkeleton('"'); - } else { // string is empty string - writeSegment(word); - } - quotes++; - } else { // midle of string - if (character == '\\') { // if is escape character - lastChar = character; - character = (char) buffer.read(); - if (character == '\n' || character == '\r') { - while (blankChar(character) && character != ' ') { - character = (char) buffer.read(); - } - if (character == '"') {// if end of string in first character of the next line - if (word.equals("")) { - writeSkeleton('"'); - writeSkeleton('"'); - } else { // string is empty string - writeSegment(word); - } - } else { // normal character in the line below \ - word = word.concat(String.valueOf(character)); - } - } else { // Normal escape character - word = word.concat(String.valueOf(lastChar)); - word = word.concat(String.valueOf(character)); - } - } else { // Normal character - word = word.concat(String.valueOf(character)); - } - } - } - } - } - - private static void parseStringTable() throws IOException { - String word = " "; - while (!beginBlock(word)) { - word = parseWords(" \n\t\r,", true, false); - } - - while (!endBlock(word)) { - stack = ""; - word = parseWords(" \n\t\r,\"L", false, true); - if (word.trim().equals("\"")) { - captureString(true); - } else { // is END or ID - writeSkeleton(stack); - } - } - } - - private static void parseMenu() throws IOException { - String word = " "; - while (!beginBlock(word)) { - word = parseWords(" \n\t\r,", true, false); - } - parseMenuBlock(); - } - - private static void parseMenuBlock() throws IOException { - String word = " "; - while (!endBlock(word)) { - word = parseWords(" ,\n\t\r\"", false, true); - if (word.trim().equals("MENUITEM")) { - writeSkeleton(stack); - stack = ""; - word = parseWords(" ,\n\t\r\"L", false, true); - if (word.trim().equals("\"")) { - stack = ""; - captureString(true); - } else { // SEPARATOR - writeSkeleton(stack); - stack = ""; - } - } else if (word.trim().equals("POPUP")) { - writeSkeleton(stack); - stack = ""; - parsePopup(); - } else if (word.trim().equals("\"")) { - stack = ""; - captureString(true); - } else { - writeSkeleton(stack); - stack = ""; - } - } - } - - private static void parsePopup() throws IOException { - String word = " "; - - while (!beginBlock(word)) { - stack = ""; - word = parseWords(" \n\t\r,\"L", false, true); - if (word.trim().equals("\"")) { - captureString(true); - } else { // is END or ID - writeSkeleton(stack); - } - } - - stack = ""; - parseMenuBlock(); - } - - private static void parseDlgInit() throws IOException { - String word = ""; - while (buffer.ready() && !beginBlock(word)) { - word = parseWords(" \n\t\r,", true, false); - } - - parseDlgInitBlock(); - } - - private static void parseDlgInitBlock() throws IOException { - String word = ""; - int position = 0; // parse position in the dlginitblock - int dataLength = 0; - stack = ""; - while (buffer.ready() && !endBlock(word)) { - word = parseWords(" \n\t\r,", false, false).trim(); - if (!word.equals("")) { - if (word.equals("0") && position == 0) { - break; - } - switch (position) { - case 0: // id - dataLength = 0; - position++; - break; - case 1: // type - if (word.equals("0x403") || word.equals("0x1234")) { - position++; - } else { - position = 6; - } - writeSkeleton(stack); - stack = ""; - break; - - case 2: // length - if (validateNumber(word)) { - dataLength = Integer.parseInt(word); - position++; - } else { - position = 6; - } - break; - case 3: // end of align 0 - position++; - break; - case 4: // first time data - if (word.charAt(0) == '\"') { // case "/000" - position = 0; - writeSkeleton(stack); - stack = ""; - break; - } - stack = ""; - writeSkeleton("###" + segId + "###,0 \r\n"); - // go to the next case now without break - case 5: // midle of data - extractString(word, dataLength); - position = 0; - stack = ""; - break; - case 6: // not string - writeSkeleton(stack); - stack = ""; - break; - } - } - } - writeSkeleton(stack); - stack = ""; - } - - private static void extractString(String ini, int dataLength) throws IOException { - byte[] array = new byte[dataLength]; - String word = ""; - int i = 1; - int length = 0; - array[length++] = (byte) Integer.decode(ini).intValue(); - array[length++] = (byte) (Integer.decode(ini).intValue() >> 8); - while (i < dataLength / 2 && buffer.ready()) { - word = parseWords(",\"", false, false).trim(); - if (word.charAt(1) == 'x' && word.length() > 3) { - array[length++] = (byte) Integer.decode(word).intValue(); - array[length++] = (byte) (Integer.decode(word).intValue() >> 8); - } - i++; - } - if (dataLength % 2 > 0) { - while (!word.equals("\"000\"")) { - word = parseWords(",\n\t\r ", false, false); - } - } - writeSegment(new String(array, StandardCharsets.UTF_8)); - } - - private static boolean validateNumber(String num) { - try { - Integer.parseInt(num); - return true; - } catch (NumberFormatException e) { - return false; - } - } - -} +/******************************************************************************* + * Copyright (c) 2003-2019 Maxprograms. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/org/documents/epl-v10.html + * + * Contributors: + * Maxprograms - initial API and implementation + *******************************************************************************/ +/** + * + */ +package com.maxprograms.converters.rc; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Hashtable; +import java.util.Vector; +import java.lang.System.Logger.Level; +import java.lang.System.Logger; + +import com.maxprograms.converters.Constants; +import com.maxprograms.converters.Utils; + +public class Rc2Xliff { + + private static InputStreamReader buffer; + private static FileOutputStream output; + private static FileOutputStream skeleton; + private static String lastWord = ""; + private static String sourceLanguage; + private static int segId; + private static String stack; + private static int blockStack; + + private Rc2Xliff() { + // do not instantiate this class + // use run method instead + } + + public static Vector run(Hashtable params) { + Vector result = new Vector<>(); + segId = 0; + String inputFile = params.get("source"); + String xliffFile = params.get("xliff"); + String skeletonFile = params.get("skeleton"); + sourceLanguage = params.get("srcLang"); + String targetLanguage = params.get("tgtLang"); + String srcEncoding = params.get("srcEncoding"); + String tgtLang = ""; + if (targetLanguage != null) { + tgtLang = "\" target-language=\"" + targetLanguage; + } + + try { + try (FileInputStream input = new FileInputStream(inputFile)) { + buffer = new InputStreamReader(input, srcEncoding); + output = new FileOutputStream(xliffFile); + stack = ""; + writeString("\n"); + writeString("\n"); + writeString("\n"); + + writeString("\n"); + writeString("
\n"); + writeString(" \n"); + writeString(" \n"); + writeString(" \n"); + writeString("
\n"); + writeString("\n"); + + skeleton = new FileOutputStream(skeletonFile); + + parseRC(); + + skeleton.close(); + + writeString("\n"); + writeString("
\n"); + writeString("
"); + buffer.close(); + } + output.close(); + result.add(Constants.SUCCESS); + } catch (IOException e) { + Logger logger = System.getLogger(Rc2Xliff.class.getName()); + logger.log(Level.ERROR, "Error convering RC file", e); + result.add(Constants.ERROR); + result.add(e.getMessage()); + } + return result; + } + + private static void writeString(String string) throws IOException { + output.write(string.getBytes(StandardCharsets.UTF_8)); + } + + private static void writeSkeleton(String string) throws IOException { + skeleton.write(string.getBytes(StandardCharsets.UTF_8)); + } + + private static void writeSkeleton(char character) throws IOException { + writeSkeleton(String.valueOf(character)); + } + + private static void writeSegment(String segment) throws IOException { + if (segment.equals("")) { + return; + } + writeString(" \n" + " " + Utils.cleanString(segment) + "\n" + " \n"); + writeSkeleton("%%%" + segId++ + "%%%"); + } + + private static void parseRC() throws IOException { + char character; + while (buffer.ready()) { + character = (char) buffer.read(); + if (character == '#') {// directives + parseDirective(); + } else if (character == '/') {// comments + // comment /* or // + parseComment(); // Keep the state + } else if (!blankChar(character)) { + parseStatement(character); + } else { + writeSkeleton(character); + } + } + } + + private static void parseComment() throws IOException { + writeSkeleton("/"); // Last character read + + boolean bLargeComment; + char prevChar = ' '; + char character; + + if (buffer.ready()) { + character = (char) buffer.read(); + writeSkeleton(character); + bLargeComment = character == '*'; // Large comment /* */ + + // Add the comment to the skeleton + while (buffer.ready()) { + character = (char) buffer.read(); + writeSkeleton(character); + if (bLargeComment && prevChar == '*' && character == '/') { + break; + } else if (!bLargeComment && (character == '\n' || character == '\r')) { + break; + } + prevChar = character; + } + } + } + + private static boolean blankChar(char c) { + return c == ' ' || c == '\n' || c == '\t' || c == '\r'; + } + + private static boolean beginBlock(String word) { + return word.trim().equals("BEGIN") || word.trim().equals("{"); + } + + private static boolean endBlock(String word) { + return word.trim().equals("END") || word.trim().equals("}"); + } + + private static void parseStatement(char initial) throws IOException { + String statement = String.valueOf(initial); + writeSkeleton(initial); + statement = statement.concat(parseWords(" ,\n\r\t", true, false)); + if (statement.trim().equals("STRINGTABLE")) { + parseStringTable(); + } else if (statement.trim().equals("DIALOG") || statement.trim().equals("DIALOGEX")) { + parseDialog(); + } else if (statement.trim().equals("MENU") || statement.trim().equals("MENUEX")) { + parseMenu(); + } else if (statement.trim().equals("POPUP")) { + parsePopup(); + } else if (statement.trim().equals("DLGINIT")) { + parseDlgInit(); + } else if (beginBlock(statement.trim())) {// BEGIN + blockStack = 0; + parseBlock(); + } + } + + private static void parseDirective() throws IOException { + char character = ' '; + writeSkeleton('#'); + String statement = parseWords(" \t", true, false); + if (statement.trim().equals("define")) { + parseDefine(); + } else { + while (buffer.ready() && character != '\r' && character != '\n') { + character = (char) buffer.read(); + writeSkeleton(character); + } + } + } + + private static void parseDefine() throws IOException { + String word = ""; + while (buffer.ready()) { + stack = ""; + word = parseWords(" \n\t\r,\"L", false, true); + if (word.trim().equals("\"")) { + captureString(true); + } else { // is END or ID + writeSkeleton(stack); + if (word.equals("\r") || word.equals("\n")) { + break; + } + } + } + } + + private static void parseBlock() throws IOException { + blockStack++; + String statement = ""; + while (blockStack != 0 && buffer.ready()) { + statement = parseWords(" \n\t\r", true, false); + if (beginBlock(statement.trim())) { + blockStack++; + } else if (endBlock(statement.trim())) { + blockStack--; + } + } + } + + private static void parseDialog() throws IOException { + parseDialogContent(); + parseControlBlock(); + } + + private static void parseDialogContent() throws IOException { + String word = " "; + while (!beginBlock(word)) { + word = parseWords(" \n\t\r(),", true, false); + if (word.trim().equals("CAPTION")) { + captureString(false); + } + } + } + + private static void parseControlBlock() throws IOException { + boolean isEnd = false; + parseWords(" (),\r\n\t", true, false); + do { + lastWord = lastWord.trim(); + if (lastWord.equals("CONTROL") || lastWord.equals("LTEXT") || lastWord.equals("CTEXT") + || lastWord.equals("RTEXT") || lastWord.equals("AUTO3STATE") || //$NON-NLS-2$ + lastWord.equals("AUTOCHECKBOX") || lastWord.equals("AUTORADIOBUTTON") || lastWord.equals("CHECKBOX") + || lastWord.equals("PUSHBOX") || lastWord.equals("PUSHBUTTON") || lastWord.equals("DEFPUSHBUTTON") + || lastWord.equals("RADIOBUTTON") || lastWord.equals("STATE3") || lastWord.equals("USERBUTTON") + || lastWord.equals("GROUPBOX")) { + isEnd = parseControlTypeI(); + } else if (lastWord.equals("EDITTEXT") || lastWord.equals("BEDIT") || lastWord.equals("IEDIT") + || lastWord.equals("HEDIT") || lastWord.equals("COMBOBOX") || lastWord.equals("LISTBOX") + || lastWord.equals("SCROLLBAR") || lastWord.equals("ICON")) { + isEnd = parseControlTypeI(); + } else { + parseWords(" (),\r\t\n", true, false); + } + if (isEnd) {// End of block in last control + break; + } + } while (true); + } + + private static boolean isEndControlStatement(String word) { + // list of all posible controls and END keyword + String[] controls = new String[] { "END", "CONTROL", "LTEXT", "CTEXT", "RTEXT", "AUTO3STATE", "AUTOCHECKBOX", //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ + "AUTORADIOBUTTON", "CHECKBOX", "PUSHBOX", "PUSHBUTTON", "DEFPUSHBUTTON", //$NON-NLS-5$ + "RADIOBUTTON", "STATE3", "USERBUTTON", "GROUPBOX", "EDITTEXT", "BEDIT", //$NON-NLS-5$ //$NON-NLS-6$ + "IEDIT", "HEDIT", "COMBOBOX", "LISTBOX", "SCROLLBAR", "ICON" }; //$NON-NLS-5$ //$NON-NLS-6$ + + for (int i = 0; i < controls.length; i++) { + if (controls[i].equals(word)) { + return true; + } + } + return false; + } + + // return true if it has a block in the control + private static boolean parseControlTypeI() throws IOException { + char cIni = ' '; + while (blankChar(cIni) && buffer.ready()) { + cIni = (char) buffer.read(); + if (cIni != '"' && cIni != 'L') { + writeSkeleton(cIni); + } + } + + // first parameter optional string + if (cIni == '"') { + captureString(true); + } else { + if (cIni == 'L') { // String type L"String" + writeSkeleton(cIni); + cIni = (char) buffer.read(); + if (cIni == '"') { + captureString(true); + } else {// don't have string + writeSkeleton(cIni); + } + } + } + + String word = " "; + boolean hasBlock = false; + while (!isEndControlStatement(word)) { + word = parseWords(" \t(),\r\n", true, false).trim(); + if (beginBlock(word)) { // begin an optional data block in the control + hasBlock = true; + } + } + return !hasBlock && endBlock(word);// end of control block? + } + + private static void writeConditional(char character, boolean write) throws IOException { + if (write) { + writeSkeleton(character); + } else { + stack += String.valueOf(character); + } + + } + + private static void parseComment(boolean write, boolean large) throws IOException { + writeConditional('/', write); // Last character read + if (large) { + writeConditional('*', write); + } else { + writeConditional('/', write); + } + + char prevChar = ' '; + char character; + + // Add the comment to the skeleton + while (buffer.ready()) { + character = (char) buffer.read(); + writeConditional(character, write); + if (large && prevChar == '*' && character == '/') { + break; + } else if (!large && (character == '\n' || character == '\r')) { + break; + } + prevChar = character; + } + + } + + private static String parseWords(String separators, boolean write, boolean withSeparator) throws IOException { + String word = ""; + char lastChar; + char character = 'a'; // initial value any character not in separators + while (buffer.ready() && separators.indexOf(character) == -1) { + character = (char) buffer.read(); + if (character == '/') { + lastChar = character; + character = (char) buffer.read(); + if (character == '/') { + parseComment(write, false); + break; + } else if (character == '*') { // skip comments + parseComment(write, true); + break; + } else { // write the last two characters + if (write) { + writeSkeleton(lastChar); + writeSkeleton(character); + } else { + stack += String.valueOf(lastChar); + stack += String.valueOf(character); + } + word = word.concat(String.valueOf(lastChar)); + if (separators.indexOf(character) == -1 || withSeparator) { // return with the separator or not + word = word.concat(String.valueOf(character)); + } + } + } else if (character == '\\') { + lastChar = character; + character = (char) buffer.read(); + if (character != '\r' || character != '\n') { + if (write) { // must write and \character + writeSkeleton(lastChar); + writeSkeleton(character); + while (blankChar(character) && character != ' ') { + character = (char) buffer.read(); + writeSkeleton(character); + } + word = word.concat(String.valueOf(character)); + } else {// not write and \ character + stack += String.valueOf(lastChar); + stack += String.valueOf(character); + while (blankChar(character)) { + character = (char) buffer.read(); + stack += String.valueOf(character); + } + word = word.concat(String.valueOf(character)); + } + } else { + if (write) { + writeSkeleton(lastChar); + writeSkeleton(character); + } else { + stack += String.valueOf(lastChar); + stack += String.valueOf(character); + } + word = word.concat(String.valueOf(lastChar)); + if (separators.indexOf(character) == -1 || withSeparator) { // return with the separator or not + word = word.concat(String.valueOf(character)); + } + } + } else { // not is a special character (comment or \) + + if (write) { + writeSkeleton(character); + } else { + + stack += String.valueOf(character); + } + if (separators.indexOf(character) == -1 || withSeparator) { // return with the separator or not + word = word.concat(String.valueOf(character)); + } + } + } + lastWord = word; + return word; + } + + private static void captureString(boolean startNow) throws IOException { + int quotes = 0; + if (startNow) { + quotes = 1; // now in the string + } + char character; + char lastChar; + String word = ""; + while (buffer.ready() && quotes < 2) { + character = (char) buffer.read(); + if (quotes == 0) { // not in the string yet + if (character == '"') {// begining of string + quotes++; + } else {// not in the string yet + writeSkeleton(character); + } + } else { // is in the string + if (character == '"') { // end of string Careful can be the \" escape character + if (word.equals("")) { + writeSkeleton('"'); + writeSkeleton('"'); + } else { // string is empty string + writeSegment(word); + } + quotes++; + } else { // midle of string + if (character == '\\') { // if is escape character + lastChar = character; + character = (char) buffer.read(); + if (character == '\n' || character == '\r') { + while (blankChar(character) && character != ' ') { + character = (char) buffer.read(); + } + if (character == '"') {// if end of string in first character of the next line + if (word.equals("")) { + writeSkeleton('"'); + writeSkeleton('"'); + } else { // string is empty string + writeSegment(word); + } + } else { // normal character in the line below \ + word = word.concat(String.valueOf(character)); + } + } else { // Normal escape character + word = word.concat(String.valueOf(lastChar)); + word = word.concat(String.valueOf(character)); + } + } else { // Normal character + word = word.concat(String.valueOf(character)); + } + } + } + } + } + + private static void parseStringTable() throws IOException { + String word = " "; + while (!beginBlock(word)) { + word = parseWords(" \n\t\r,", true, false); + } + + while (!endBlock(word)) { + stack = ""; + word = parseWords(" \n\t\r,\"L", false, true); + if (word.trim().equals("\"")) { + captureString(true); + } else { // is END or ID + writeSkeleton(stack); + } + } + } + + private static void parseMenu() throws IOException { + String word = " "; + while (!beginBlock(word)) { + word = parseWords(" \n\t\r,", true, false); + } + parseMenuBlock(); + } + + private static void parseMenuBlock() throws IOException { + String word = " "; + while (!endBlock(word)) { + word = parseWords(" ,\n\t\r\"", false, true); + if (word.trim().equals("MENUITEM")) { + writeSkeleton(stack); + stack = ""; + word = parseWords(" ,\n\t\r\"L", false, true); + if (word.trim().equals("\"")) { + stack = ""; + captureString(true); + } else { // SEPARATOR + writeSkeleton(stack); + stack = ""; + } + } else if (word.trim().equals("POPUP")) { + writeSkeleton(stack); + stack = ""; + parsePopup(); + } else if (word.trim().equals("\"")) { + stack = ""; + captureString(true); + } else { + writeSkeleton(stack); + stack = ""; + } + } + } + + private static void parsePopup() throws IOException { + String word = " "; + + while (!beginBlock(word)) { + stack = ""; + word = parseWords(" \n\t\r,\"L", false, true); + if (word.trim().equals("\"")) { + captureString(true); + } else { // is END or ID + writeSkeleton(stack); + } + } + + stack = ""; + parseMenuBlock(); + } + + private static void parseDlgInit() throws IOException { + String word = ""; + while (buffer.ready() && !beginBlock(word)) { + word = parseWords(" \n\t\r,", true, false); + } + + parseDlgInitBlock(); + } + + private static void parseDlgInitBlock() throws IOException { + String word = ""; + int position = 0; // parse position in the dlginitblock + int dataLength = 0; + stack = ""; + while (buffer.ready() && !endBlock(word)) { + word = parseWords(" \n\t\r,", false, false).trim(); + if (!word.equals("")) { + if (word.equals("0") && position == 0) { + break; + } + switch (position) { + case 0: // id + dataLength = 0; + position++; + break; + case 1: // type + if (word.equals("0x403") || word.equals("0x1234")) { + position++; + } else { + position = 6; + } + writeSkeleton(stack); + stack = ""; + break; + + case 2: // length + if (validateNumber(word)) { + dataLength = Integer.parseInt(word); + position++; + } else { + position = 6; + } + break; + case 3: // end of align 0 + position++; + break; + case 4: // first time data + if (word.charAt(0) == '\"') { // case "/000" + position = 0; + writeSkeleton(stack); + stack = ""; + break; + } + stack = ""; + writeSkeleton("###" + segId + "###,0 \r\n"); + // go to the next case now without break + case 5: // midle of data + extractString(word, dataLength); + position = 0; + stack = ""; + break; + case 6: // not string + writeSkeleton(stack); + stack = ""; + break; + } + } + } + writeSkeleton(stack); + stack = ""; + } + + private static void extractString(String ini, int dataLength) throws IOException { + byte[] array = new byte[dataLength]; + String word = ""; + int i = 1; + int length = 0; + array[length++] = (byte) Integer.decode(ini).intValue(); + array[length++] = (byte) (Integer.decode(ini).intValue() >> 8); + while (i < dataLength / 2 && buffer.ready()) { + word = parseWords(",\"", false, false).trim(); + if (word.charAt(1) == 'x' && word.length() > 3) { + array[length++] = (byte) Integer.decode(word).intValue(); + array[length++] = (byte) (Integer.decode(word).intValue() >> 8); + } + i++; + } + if (dataLength % 2 > 0) { + while (!word.equals("\"000\"")) { + word = parseWords(",\n\t\r ", false, false); + } + } + writeSegment(new String(array, StandardCharsets.UTF_8)); + } + + private static boolean validateNumber(String num) { + try { + Integer.parseInt(num); + return true; + } catch (NumberFormatException e) { + return false; + } + } + +} diff --git a/src/com/maxprograms/converters/rc/Xliff2Rc.java b/src/com/maxprograms/converters/rc/Xliff2Rc.java index 1f63f9a8..4e1904c6 100644 --- a/src/com/maxprograms/converters/rc/Xliff2Rc.java +++ b/src/com/maxprograms/converters/rc/Xliff2Rc.java @@ -1,267 +1,268 @@ -/******************************************************************************* - * Copyright (c) 2003-2019 Maxprograms. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 1.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/org/documents/epl-v10.html - * - * Contributors: - * Maxprograms - initial API and implementation - *******************************************************************************/ -package com.maxprograms.converters.rc; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.UnsupportedEncodingException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.List; -import java.util.Vector; -import java.lang.System.Logger.Level; -import java.lang.System.Logger; - -import javax.xml.parsers.ParserConfigurationException; - -import org.xml.sax.SAXException; - -import com.maxprograms.converters.UnexistentSegmentException; -import com.maxprograms.xml.Catalog; -import com.maxprograms.xml.Document; -import com.maxprograms.xml.Element; -import com.maxprograms.xml.SAXBuilder; - -public class Xliff2Rc { - - private static String sklFile; - private static String xliffFile; - - private static Hashtable segments; - private static FileOutputStream output; - private static String catalog; - private static Hashtable dlgText; - private static String destTemp; - private static String encoding; - - private Xliff2Rc() { - // do not instantiate this class - // use run method instead - } - - public static Vector run(Hashtable params) { - - Vector result = new Vector<>(); - - dlgText = new Hashtable<>(); - - sklFile = params.get("skeleton"); - xliffFile = params.get("xliff"); - catalog = params.get("catalog"); - encoding = params.get("encoding"); - - try { - - File tempFile = File.createTempFile("tempRC", ".temp"); - destTemp = tempFile.getAbsolutePath(); - output = new FileOutputStream(destTemp); - loadSegments(); - - dlgInitExists(params); - - try (InputStreamReader input = new InputStreamReader(new FileInputStream(sklFile), - StandardCharsets.UTF_8)) { - BufferedReader buffer = new BufferedReader(input); - - String line; - while ((line = buffer.readLine()) != null) { - line = line + "\n"; - - if (line.indexOf("%%%") != -1) { - // - // contains translatable text - // - int index = line.indexOf("%%%"); - while (index != -1) { - String start = line.substring(0, index); - writeString(start); - line = line.substring(index + 3); - String code = line.substring(0, line.indexOf("%%%")); - line = line.substring(line.indexOf("%%%") + 3); - Element segment = segments.get(code); - if (segment != null) { - - if (segment.getAttributeValue("approved", "no").equalsIgnoreCase("Yes")) { - Element target = segment.getChild("target"); - if (target != null) { - writeString(converDlgInit(target.getText(), code)); - } else { - throw new UnexistentSegmentException(); - } - } else { - // process source - Element source = segment.getChild("source"); - writeString(converDlgInit(source.getText(), code)); - } - } else { - throw new UnexistentSegmentException(); - } - - index = line.indexOf("%%%"); - if (index == -1) { - writeString(line); - } - } // end while - - } else { - writeString(line); - } - } - } - output.close(); - dlgInitLengths(params); - Files.delete(Paths.get(tempFile.toURI())); - result.add("0"); - } catch (IOException | SAXException | ParserConfigurationException | UnexistentSegmentException e) { - Logger logger = System.getLogger(Xliff2Rc.class.getName()); - logger.log(Level.ERROR, "Error merging RC file", e); - result.add("1"); - result.add(e.getMessage()); - } - return result; - } - - private static String converDlgInit(String word, String code) throws UnsupportedEncodingException { - if (dlgText.containsKey(code)) { - return decode(word, code); - } - return "\"" + word + "\""; - } - - private static String decode(String word, String code) throws UnsupportedEncodingException { - dlgText.remove(code); - byte[] bytes = word.getBytes("UTF-8"); - String byteWord = ""; - Byte par = Byte.valueOf("00"); - int length = bytes.length; - - for (int i = 0; i < length; i = i + 1) { - if (i % 2 == 0) { - par = Byte.valueOf(bytes[i]); - } else { - Byte impar = Byte.valueOf(bytes[i]); - byteWord = byteWord + " 0x" + Integer.toHexString(impar.intValue()) - + Integer.toHexString(par.intValue()) + ", "; - } - } - - if (bytes.length % 2 == 0) { - byteWord = byteWord + "\"\\000\" "; - } else { - if (length > 0) { - byteWord = byteWord + " 0x00" + Integer.toHexString(par.intValue()) + ", "; - } - byteWord = byteWord + " "; - } - length++; - dlgText.put(code, Integer.toString(length)); - return byteWord; - } - - private static void loadSegments() throws SAXException, IOException, ParserConfigurationException { - - SAXBuilder builder = new SAXBuilder(); - builder.setEntityResolver(new Catalog(catalog)); - - Document doc = builder.build(xliffFile); - Element root = doc.getRootElement(); - Element body = root.getChild("file").getChild("body"); - List units = body.getChildren("trans-unit"); - Iterator i = units.iterator(); - - segments = new Hashtable<>(); - - while (i.hasNext()) { - Element unit = i.next(); - segments.put(unit.getAttributeValue("id"), unit); - } - } - - private static void writeStringEncoded(String string) throws IOException { - output.write(string.getBytes(encoding)); - } - - private static void writeString(String string) throws IOException { - output.write(string.getBytes(StandardCharsets.UTF_8)); - } - - private static void dlgInitExists(Hashtable params) throws IOException, UnexistentSegmentException { - sklFile = params.get("skeleton"); - xliffFile = params.get("xliff"); - catalog = params.get("catalog"); - try (InputStreamReader input = new InputStreamReader(new FileInputStream(sklFile), StandardCharsets.UTF_8)) { - BufferedReader buffer = new BufferedReader(input); - String line; - while ((line = buffer.readLine()) != null) { - if (line.indexOf("###") != -1) { - // contains dlginit length - int index = line.indexOf("###"); - while (index != -1) { - line = line.substring(index + 3); - String code = line.substring(0, line.indexOf("###")); - line = line.substring(line.indexOf("###") + 3); - Element segment = segments.get(code); - if (segment != null) { - dlgText.put(code, Integer.valueOf(0)); - } else { - throw new UnexistentSegmentException("Missing segment " + code); - } - index = line.indexOf("###"); - } // end while - } - } - } - } - - private static void dlgInitLengths(Hashtable params) throws IOException { - sklFile = params.get("skeleton"); - xliffFile = params.get("xliff"); - catalog = params.get("catalog"); - - String outputFile = params.get("backfile"); - output = new FileOutputStream(outputFile); - try (InputStreamReader input = new InputStreamReader(new FileInputStream(destTemp), StandardCharsets.UTF_8)) { - BufferedReader buffer = new BufferedReader(input); - String line; - while ((line = buffer.readLine()) != null) { - line = line + "\n"; - - if (line.indexOf("###") != -1) { - // contains dlginit length - int index = line.indexOf("###"); - while (index != -1) { - String start = line.substring(0, index); - writeStringEncoded(start); - line = line.substring(index + 3); - String code = line.substring(0, line.indexOf("###")); - line = line.substring(line.indexOf("###") + 3); - writeStringEncoded((String) dlgText.get(code)); - index = line.indexOf("###"); - if (index == -1) { - writeStringEncoded(line); - } - } // end while - - } else { - writeStringEncoded(line); - } - } - } - } -} +/******************************************************************************* + * Copyright (c) 2003-2019 Maxprograms. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/org/documents/epl-v10.html + * + * Contributors: + * Maxprograms - initial API and implementation + *******************************************************************************/ +package com.maxprograms.converters.rc; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Vector; +import java.lang.System.Logger.Level; +import java.lang.System.Logger; + +import javax.xml.parsers.ParserConfigurationException; + +import org.xml.sax.SAXException; + +import com.maxprograms.converters.Constants; +import com.maxprograms.converters.UnexistentSegmentException; +import com.maxprograms.xml.Catalog; +import com.maxprograms.xml.Document; +import com.maxprograms.xml.Element; +import com.maxprograms.xml.SAXBuilder; + +public class Xliff2Rc { + + private static String sklFile; + private static String xliffFile; + + private static Hashtable segments; + private static FileOutputStream output; + private static String catalog; + private static Hashtable dlgText; + private static String destTemp; + private static String encoding; + + private Xliff2Rc() { + // do not instantiate this class + // use run method instead + } + + public static Vector run(Hashtable params) { + + Vector result = new Vector<>(); + + dlgText = new Hashtable<>(); + + sklFile = params.get("skeleton"); + xliffFile = params.get("xliff"); + catalog = params.get("catalog"); + encoding = params.get("encoding"); + + try { + + File tempFile = File.createTempFile("tempRC", ".temp"); + destTemp = tempFile.getAbsolutePath(); + output = new FileOutputStream(destTemp); + loadSegments(); + + dlgInitExists(params); + + try (InputStreamReader input = new InputStreamReader(new FileInputStream(sklFile), + StandardCharsets.UTF_8)) { + BufferedReader buffer = new BufferedReader(input); + + String line; + while ((line = buffer.readLine()) != null) { + line = line + "\n"; + + if (line.indexOf("%%%") != -1) { + // + // contains translatable text + // + int index = line.indexOf("%%%"); + while (index != -1) { + String start = line.substring(0, index); + writeString(start); + line = line.substring(index + 3); + String code = line.substring(0, line.indexOf("%%%")); + line = line.substring(line.indexOf("%%%") + 3); + Element segment = segments.get(code); + if (segment != null) { + + if (segment.getAttributeValue("approved", "no").equalsIgnoreCase("Yes")) { + Element target = segment.getChild("target"); + if (target != null) { + writeString(converDlgInit(target.getText(), code)); + } else { + throw new UnexistentSegmentException(); + } + } else { + // process source + Element source = segment.getChild("source"); + writeString(converDlgInit(source.getText(), code)); + } + } else { + throw new UnexistentSegmentException(); + } + + index = line.indexOf("%%%"); + if (index == -1) { + writeString(line); + } + } // end while + + } else { + writeString(line); + } + } + } + output.close(); + dlgInitLengths(params); + Files.delete(Paths.get(tempFile.toURI())); + result.add(Constants.SUCCESS); + } catch (IOException | SAXException | ParserConfigurationException | UnexistentSegmentException e) { + Logger logger = System.getLogger(Xliff2Rc.class.getName()); + logger.log(Level.ERROR, "Error merging RC file", e); + result.add(Constants.ERROR); + result.add(e.getMessage()); + } + return result; + } + + private static String converDlgInit(String word, String code) throws UnsupportedEncodingException { + if (dlgText.containsKey(code)) { + return decode(word, code); + } + return "\"" + word + "\""; + } + + private static String decode(String word, String code) throws UnsupportedEncodingException { + dlgText.remove(code); + byte[] bytes = word.getBytes("UTF-8"); + String byteWord = ""; + Byte par = Byte.valueOf("00"); + int length = bytes.length; + + for (int i = 0; i < length; i = i + 1) { + if (i % 2 == 0) { + par = Byte.valueOf(bytes[i]); + } else { + Byte impar = Byte.valueOf(bytes[i]); + byteWord = byteWord + " 0x" + Integer.toHexString(impar.intValue()) + + Integer.toHexString(par.intValue()) + ", "; + } + } + + if (bytes.length % 2 == 0) { + byteWord = byteWord + "\"\\000\" "; + } else { + if (length > 0) { + byteWord = byteWord + " 0x00" + Integer.toHexString(par.intValue()) + ", "; + } + byteWord = byteWord + " "; + } + length++; + dlgText.put(code, Integer.toString(length)); + return byteWord; + } + + private static void loadSegments() throws SAXException, IOException, ParserConfigurationException { + + SAXBuilder builder = new SAXBuilder(); + builder.setEntityResolver(new Catalog(catalog)); + + Document doc = builder.build(xliffFile); + Element root = doc.getRootElement(); + Element body = root.getChild("file").getChild("body"); + List units = body.getChildren("trans-unit"); + Iterator i = units.iterator(); + + segments = new Hashtable<>(); + + while (i.hasNext()) { + Element unit = i.next(); + segments.put(unit.getAttributeValue("id"), unit); + } + } + + private static void writeStringEncoded(String string) throws IOException { + output.write(string.getBytes(encoding)); + } + + private static void writeString(String string) throws IOException { + output.write(string.getBytes(StandardCharsets.UTF_8)); + } + + private static void dlgInitExists(Hashtable params) throws IOException, UnexistentSegmentException { + sklFile = params.get("skeleton"); + xliffFile = params.get("xliff"); + catalog = params.get("catalog"); + try (InputStreamReader input = new InputStreamReader(new FileInputStream(sklFile), StandardCharsets.UTF_8)) { + BufferedReader buffer = new BufferedReader(input); + String line; + while ((line = buffer.readLine()) != null) { + if (line.indexOf("###") != -1) { + // contains dlginit length + int index = line.indexOf("###"); + while (index != -1) { + line = line.substring(index + 3); + String code = line.substring(0, line.indexOf("###")); + line = line.substring(line.indexOf("###") + 3); + Element segment = segments.get(code); + if (segment != null) { + dlgText.put(code, Integer.valueOf(0)); + } else { + throw new UnexistentSegmentException("Missing segment " + code); + } + index = line.indexOf("###"); + } // end while + } + } + } + } + + private static void dlgInitLengths(Hashtable params) throws IOException { + sklFile = params.get("skeleton"); + xliffFile = params.get("xliff"); + catalog = params.get("catalog"); + + String outputFile = params.get("backfile"); + output = new FileOutputStream(outputFile); + try (InputStreamReader input = new InputStreamReader(new FileInputStream(destTemp), StandardCharsets.UTF_8)) { + BufferedReader buffer = new BufferedReader(input); + String line; + while ((line = buffer.readLine()) != null) { + line = line + "\n"; + + if (line.indexOf("###") != -1) { + // contains dlginit length + int index = line.indexOf("###"); + while (index != -1) { + String start = line.substring(0, index); + writeStringEncoded(start); + line = line.substring(index + 3); + String code = line.substring(0, line.indexOf("###")); + line = line.substring(line.indexOf("###") + 3); + writeStringEncoded((String) dlgText.get(code)); + index = line.indexOf("###"); + if (index == -1) { + writeStringEncoded(line); + } + } // end while + + } else { + writeStringEncoded(line); + } + } + } + } +} diff --git a/src/com/maxprograms/converters/resx/Resx2Xliff.java b/src/com/maxprograms/converters/resx/Resx2Xliff.java index a9ef7341..40d053bd 100644 --- a/src/com/maxprograms/converters/resx/Resx2Xliff.java +++ b/src/com/maxprograms/converters/resx/Resx2Xliff.java @@ -1,148 +1,149 @@ -/******************************************************************************* - * Copyright (c) 2003-2019 Maxprograms. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 1.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/org/documents/epl-v10.html - * - * Contributors: - * Maxprograms - initial API and implementation - *******************************************************************************/ -package com.maxprograms.converters.resx; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.Hashtable; -import java.util.List; -import java.util.Vector; -import java.lang.System.Logger.Level; -import java.lang.System.Logger; - -import javax.xml.parsers.ParserConfigurationException; - -import org.xml.sax.SAXException; - -import com.maxprograms.converters.xml.Xml2Xliff; -import com.maxprograms.xml.Catalog; -import com.maxprograms.xml.Document; -import com.maxprograms.xml.Element; -import com.maxprograms.xml.SAXBuilder; -import com.maxprograms.xml.XMLNode; -import com.maxprograms.xml.XMLOutputter; - -public class Resx2Xliff { - - private Resx2Xliff() { - // do not instantiate this class - // use run method instead - } - - public static Vector run(Hashtable params) { - Vector result = new Vector<>(); - try { - String inputFile = params.get("source"); - String catalog = params.get("catalog"); - - Document xmlResx = openXml(inputFile, catalog); - - Element root = xmlResx.getRootElement(); - List lstNodes = root.getChildren(); - for (int i = 0; i < lstNodes.size(); i++) { - Element node = lstNodes.get(i); - if (node.getName().equals("data") && isTrans(node) && !isSkipCommand(node)) { - List lstChilds = node.getContent(); - for (int j = 0; j < lstChilds.size(); j++) { - XMLNode childNode = lstChilds.get(j); - if (childNode.getNodeType() == XMLNode.ELEMENT_NODE) { - Element eChild = (Element) childNode; - if (eChild.getName().equals("value")) { - Element newChild = new Element("translate"); - newChild.setContent(eChild.getContent()); - lstChilds.set(j, newChild); - } - } - } - node.setContent(lstChilds); - } - } - String original = inputFile; - File tempFile = File.createTempFile("tempResx", ".xml"); - inputFile = tempFile.getAbsolutePath(); - saveXml(xmlResx, inputFile); - params.put("source", inputFile); - params.put("resx", "yes"); - result = Xml2Xliff.run(params); - Files.delete(Paths.get(tempFile.toURI())); - if ("0".equals(result.get(0))) { - setOriginal(params.get("xliff"), original); - } - } catch (IOException | ParserConfigurationException | SAXException e) { - Logger logger = System.getLogger(Resx2Xliff.class.getName()); - logger.log(Level.ERROR, "Error converting ResX file", e); - result.add("1"); - result.add(e.getMessage()); - } - return result; - } - - private static void setOriginal(String xliff, String original) - throws IOException, SAXException, ParserConfigurationException { - SAXBuilder builder = new SAXBuilder(); - Document doc = builder.build(xliff); - doc.getRootElement().getChild("file").setAttribute("original", original); - saveXml(doc, xliff); - } - - /* - * Each data row contains a name, and value. The row also contains a type or - * mimetype. Type corresponds to a .NET class that support text/value - * conversion. Classes that don't support this are serialized and stored with - * the mimetype set. - */ - - static boolean isTrans(Element node) { - if (node.getAttributeValue("name", "").startsWith(">>")) { - return false; - } - if (node.getAttribute("mimetype") != null && !node.getAttributeValue("mimetype").trim().equals("")) { - return false; - } - if (node.getAttribute("type") != null && !node.getAttributeValue("type").trim().equals("")) { - return false; - } - return true; - } - - static boolean isSkipCommand(Element node) { - // Search for the _skip command in comment tag - List children = node.getChildren("comment"); - for (int i = 0; i < children.size(); i++) { - if (children.get(i).getText().equalsIgnoreCase("_skip")) { - return true; - } - } - return false; - } - - static Document openXml(String filename, String catalog) - throws ParserConfigurationException, SAXException, IOException { - SAXBuilder builder = new SAXBuilder(); - builder.setValidating(false); - builder.setEntityResolver(new Catalog(catalog)); - return builder.build(filename); - } - - // Save the xml to a file - static void saveXml(Document xmlDoc, String xmlFile) throws IOException { - XMLOutputter outputter = new XMLOutputter(); - outputter.preserveSpace(true); - try (FileOutputStream soutput = new FileOutputStream(xmlFile)) { - outputter.output(xmlDoc, soutput); - } - } - -} +/******************************************************************************* + * Copyright (c) 2003-2019 Maxprograms. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/org/documents/epl-v10.html + * + * Contributors: + * Maxprograms - initial API and implementation + *******************************************************************************/ +package com.maxprograms.converters.resx; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Hashtable; +import java.util.List; +import java.util.Vector; +import java.lang.System.Logger.Level; +import java.lang.System.Logger; + +import javax.xml.parsers.ParserConfigurationException; + +import org.xml.sax.SAXException; + +import com.maxprograms.converters.Constants; +import com.maxprograms.converters.xml.Xml2Xliff; +import com.maxprograms.xml.Catalog; +import com.maxprograms.xml.Document; +import com.maxprograms.xml.Element; +import com.maxprograms.xml.SAXBuilder; +import com.maxprograms.xml.XMLNode; +import com.maxprograms.xml.XMLOutputter; + +public class Resx2Xliff { + + private Resx2Xliff() { + // do not instantiate this class + // use run method instead + } + + public static Vector run(Hashtable params) { + Vector result = new Vector<>(); + try { + String inputFile = params.get("source"); + String catalog = params.get("catalog"); + + Document xmlResx = openXml(inputFile, catalog); + + Element root = xmlResx.getRootElement(); + List lstNodes = root.getChildren(); + for (int i = 0; i < lstNodes.size(); i++) { + Element node = lstNodes.get(i); + if (node.getName().equals("data") && isTrans(node) && !isSkipCommand(node)) { + List lstChilds = node.getContent(); + for (int j = 0; j < lstChilds.size(); j++) { + XMLNode childNode = lstChilds.get(j); + if (childNode.getNodeType() == XMLNode.ELEMENT_NODE) { + Element eChild = (Element) childNode; + if (eChild.getName().equals("value")) { + Element newChild = new Element("translate"); + newChild.setContent(eChild.getContent()); + lstChilds.set(j, newChild); + } + } + } + node.setContent(lstChilds); + } + } + String original = inputFile; + File tempFile = File.createTempFile("tempResx", ".xml"); + inputFile = tempFile.getAbsolutePath(); + saveXml(xmlResx, inputFile); + params.put("source", inputFile); + params.put("resx", "yes"); + result = Xml2Xliff.run(params); + Files.delete(Paths.get(tempFile.toURI())); + if (Constants.SUCCESS.equals(result.get(0))) { + setOriginal(params.get("xliff"), original); + } + } catch (IOException | ParserConfigurationException | SAXException e) { + Logger logger = System.getLogger(Resx2Xliff.class.getName()); + logger.log(Level.ERROR, "Error converting ResX file", e); + result.add(Constants.ERROR); + result.add(e.getMessage()); + } + return result; + } + + private static void setOriginal(String xliff, String original) + throws IOException, SAXException, ParserConfigurationException { + SAXBuilder builder = new SAXBuilder(); + Document doc = builder.build(xliff); + doc.getRootElement().getChild("file").setAttribute("original", original); + saveXml(doc, xliff); + } + + /* + * Each data row contains a name, and value. The row also contains a type or + * mimetype. Type corresponds to a .NET class that support text/value + * conversion. Classes that don't support this are serialized and stored with + * the mimetype set. + */ + + static boolean isTrans(Element node) { + if (node.getAttributeValue("name", "").startsWith(">>")) { + return false; + } + if (node.getAttribute("mimetype") != null && !node.getAttributeValue("mimetype").trim().equals("")) { + return false; + } + if (node.getAttribute("type") != null && !node.getAttributeValue("type").trim().equals("")) { + return false; + } + return true; + } + + static boolean isSkipCommand(Element node) { + // Search for the _skip command in comment tag + List children = node.getChildren("comment"); + for (int i = 0; i < children.size(); i++) { + if (children.get(i).getText().equalsIgnoreCase("_skip")) { + return true; + } + } + return false; + } + + static Document openXml(String filename, String catalog) + throws ParserConfigurationException, SAXException, IOException { + SAXBuilder builder = new SAXBuilder(); + builder.setValidating(false); + builder.setEntityResolver(new Catalog(catalog)); + return builder.build(filename); + } + + // Save the xml to a file + static void saveXml(Document xmlDoc, String xmlFile) throws IOException { + XMLOutputter outputter = new XMLOutputter(); + outputter.preserveSpace(true); + try (FileOutputStream soutput = new FileOutputStream(xmlFile)) { + outputter.output(xmlDoc, soutput); + } + } + +} diff --git a/src/com/maxprograms/converters/resx/Xliff2Resx.java b/src/com/maxprograms/converters/resx/Xliff2Resx.java index b100509b..38a578ff 100644 --- a/src/com/maxprograms/converters/resx/Xliff2Resx.java +++ b/src/com/maxprograms/converters/resx/Xliff2Resx.java @@ -1,101 +1,102 @@ -/******************************************************************************* - * Copyright (c) 2003-2019 Maxprograms. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 1.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/org/documents/epl-v10.html - * - * Contributors: - * Maxprograms - initial API and implementation - *******************************************************************************/ -package com.maxprograms.converters.resx; - -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.Hashtable; -import java.util.List; -import java.util.Vector; -import java.lang.System.Logger.Level; -import java.lang.System.Logger; - -import javax.xml.parsers.ParserConfigurationException; - -import org.xml.sax.SAXException; - -import com.maxprograms.converters.xml.Xliff2Xml; -import com.maxprograms.xml.Catalog; -import com.maxprograms.xml.Document; -import com.maxprograms.xml.Element; -import com.maxprograms.xml.SAXBuilder; -import com.maxprograms.xml.XMLNode; -import com.maxprograms.xml.XMLOutputter; - -public class Xliff2Resx { - - private Xliff2Resx() { - // do not instantiate this class - // use run method instead - } - - public static Vector run(Hashtable params) { - Vector result = new Vector<>(); - try { - result = Xliff2Xml.run(params); - if (!"0".equals(result.get(0))) { - return result; - } - String inputFile = params.get("backfile"); - String catalog = params.get("catalog"); - - Document xmlResx = openXml(inputFile, catalog); - Element root = xmlResx.getRootElement(); - List lstNodes = root.getChildren(); - for (int i = 0; i < lstNodes.size(); i++) { - Element node = lstNodes.get(i); - List lstChilds = node.getContent(); - for (int j = 0; j < lstChilds.size(); j++) { - if (lstChilds.get(j).getNodeType() == XMLNode.ELEMENT_NODE) { - Element child = (Element) lstChilds.get(j); - if (isConvNode(child)) { - Element newChild = new Element("value"); - newChild.setContent(child.getContent()); - lstChilds.set(j, newChild); - } - } - } - node.setContent(lstChilds); - } - - saveXml(xmlResx, inputFile); - result.add("0"); - } catch (IOException | ParserConfigurationException | SAXException e) { - Logger logger = System.getLogger(Xliff2Resx.class.getName()); - logger.log(Level.ERROR, "Error merging ResX file", e); - result.add("1"); - result.add(e.getMessage()); - } - return result; - } - - static boolean isConvNode(Element node) { - return node.getName().equals("translate"); - } - - static Document openXml(String filename, String catalog) - throws ParserConfigurationException, SAXException, IOException { - SAXBuilder builder = new SAXBuilder(); - builder.setEntityResolver(new Catalog(catalog)); - return builder.build(filename); - } - - // Save the xml to a file - static void saveXml(Document xmlDoc, String xmlFile) throws IOException { - XMLOutputter outputter = new XMLOutputter(); - outputter.preserveSpace(true); - try (FileOutputStream soutput = new FileOutputStream(xmlFile)) { - outputter.output(xmlDoc, soutput); - } - } - -} +/******************************************************************************* + * Copyright (c) 2003-2019 Maxprograms. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/org/documents/epl-v10.html + * + * Contributors: + * Maxprograms - initial API and implementation + *******************************************************************************/ +package com.maxprograms.converters.resx; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Hashtable; +import java.util.List; +import java.util.Vector; +import java.lang.System.Logger.Level; +import java.lang.System.Logger; + +import javax.xml.parsers.ParserConfigurationException; + +import org.xml.sax.SAXException; + +import com.maxprograms.converters.Constants; +import com.maxprograms.converters.xml.Xliff2Xml; +import com.maxprograms.xml.Catalog; +import com.maxprograms.xml.Document; +import com.maxprograms.xml.Element; +import com.maxprograms.xml.SAXBuilder; +import com.maxprograms.xml.XMLNode; +import com.maxprograms.xml.XMLOutputter; + +public class Xliff2Resx { + + private Xliff2Resx() { + // do not instantiate this class + // use run method instead + } + + public static Vector run(Hashtable params) { + Vector result = new Vector<>(); + try { + result = Xliff2Xml.run(params); + if (!Constants.SUCCESS.equals(result.get(0))) { + return result; + } + String inputFile = params.get("backfile"); + String catalog = params.get("catalog"); + + Document xmlResx = openXml(inputFile, catalog); + Element root = xmlResx.getRootElement(); + List lstNodes = root.getChildren(); + for (int i = 0; i < lstNodes.size(); i++) { + Element node = lstNodes.get(i); + List lstChilds = node.getContent(); + for (int j = 0; j < lstChilds.size(); j++) { + if (lstChilds.get(j).getNodeType() == XMLNode.ELEMENT_NODE) { + Element child = (Element) lstChilds.get(j); + if (isConvNode(child)) { + Element newChild = new Element("value"); + newChild.setContent(child.getContent()); + lstChilds.set(j, newChild); + } + } + } + node.setContent(lstChilds); + } + + saveXml(xmlResx, inputFile); + result.add(Constants.SUCCESS); + } catch (IOException | ParserConfigurationException | SAXException e) { + Logger logger = System.getLogger(Xliff2Resx.class.getName()); + logger.log(Level.ERROR, "Error merging ResX file", e); + result.add(Constants.ERROR); + result.add(e.getMessage()); + } + return result; + } + + static boolean isConvNode(Element node) { + return node.getName().equals("translate"); + } + + static Document openXml(String filename, String catalog) + throws ParserConfigurationException, SAXException, IOException { + SAXBuilder builder = new SAXBuilder(); + builder.setEntityResolver(new Catalog(catalog)); + return builder.build(filename); + } + + // Save the xml to a file + static void saveXml(Document xmlDoc, String xmlFile) throws IOException { + XMLOutputter outputter = new XMLOutputter(); + outputter.preserveSpace(true); + try (FileOutputStream soutput = new FileOutputStream(xmlFile)) { + outputter.output(xmlDoc, soutput); + } + } + +} diff --git a/src/com/maxprograms/converters/sdlxliff/Sdl2Xliff.java b/src/com/maxprograms/converters/sdlxliff/Sdl2Xliff.java index 87b781cd..bb2b2def 100644 --- a/src/com/maxprograms/converters/sdlxliff/Sdl2Xliff.java +++ b/src/com/maxprograms/converters/sdlxliff/Sdl2Xliff.java @@ -1,274 +1,275 @@ -/******************************************************************************* - * Copyright (c) 2003-2019 Maxprograms. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 1.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/org/documents/epl-v10.html - * - * Contributors: - * Maxprograms - initial API and implementation - *******************************************************************************/ -package com.maxprograms.converters.sdlxliff; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.List; -import java.util.Vector; -import java.lang.System.Logger.Level; -import java.lang.System.Logger; - -import javax.xml.parsers.ParserConfigurationException; - -import org.xml.sax.SAXException; - -import com.maxprograms.converters.Utils; -import com.maxprograms.xml.Catalog; -import com.maxprograms.xml.Document; -import com.maxprograms.xml.Element; -import com.maxprograms.xml.SAXBuilder; -import com.maxprograms.xml.XMLNode; -import com.maxprograms.xml.XMLOutputter; - -public class Sdl2Xliff { - - private static FileOutputStream out; - - private Sdl2Xliff() { - // do not instantiate this class - // use run method instead - } - - public static Vector run(Hashtable params) { - Vector result = new Vector<>(); - - try { - String original = params.get("source"); - String output = params.get("xliff"); - String skeletonFile = params.get("skeleton"); - String sourceLanguage = params.get("srcLang"); - String targetLanguage = params.get("tgtLang"); - String srxRules = params.get("srxFile"); - String catalog = params.get("catalog"); - String tgtLang = ""; - if (targetLanguage != null) { - tgtLang = "\" target-language=\"" + targetLanguage; - } - - XliffModel model = new XliffModel(original, srxRules, catalog); - if (model.wasModified()) { - File f = File.createTempFile("temp", ".sdlxliff"); - f.deleteOnExit(); - model.save(f.getAbsolutePath()); - original = f.getAbsolutePath(); - } - model = null; - - SAXBuilder builder = new SAXBuilder(); - builder.setEntityResolver(new Catalog(catalog)); - Document doc = builder.build(original); - - out = new FileOutputStream(output); - writeStr("\n"); - writeStr("\n"); - writeStr("\n"); - writeStr("\n"); - - writeStr("
\n"); - writeStr("\n"); - writeStr("\n"); - writeStr("\n"); - writeStr("
\n"); - writeStr("\n"); - - recurse(doc.getRootElement()); - - writeStr("\n"); - writeStr("
\n"); - writeStr("
\n"); - out.close(); - - XMLOutputter outputter = new XMLOutputter(); - outputter.preserveSpace(true); - outputter.setSkipLinefeed(true); - outputter.writeBOM(true); - try (FileOutputStream skl = new FileOutputStream(skeletonFile)) { - outputter.output(doc, skl); - } - result.add("0"); - } catch (IOException | SAXException | ParserConfigurationException e) { - Logger logger = System.getLogger(Sdl2Xliff.class.getName()); - logger.log(Level.ERROR, "Error converting SDLXLIFF file", e); - result.add("1"); - result.add(e.getMessage()); - } - return result; - } - - private static void recurse(Element root) throws IOException { - if (root.getName().equals("trans-unit")) { - if (root.getAttributeValue("translate", "yes").equals("no") - && !(root.getAttributeValue("sdl:locktype", "").equals("Manual") - && containsSrcText(root.getChild("source")))) { - return; - } - Element segSource = root.getChild("seg-source"); - Element target = root.getChild("target"); - if (segSource != null) { - if (containsText(segSource)) { - Hashtable targets = new Hashtable<>(); - if (target != null) { - List tmarks = getSegments(target); - Iterator tt = tmarks.iterator(); - while (tt.hasNext()) { - Element mrk = tt.next(); - targets.put(mrk.getAttributeValue("mid"), mrk); - } - } - List mrks = getSegments(segSource); - if (!mrks.isEmpty()) { - Iterator it = mrks.iterator(); - while (it.hasNext()) { - Element mrk = it.next(); - writeStr("\n"); - // write new source - writeStr(""); - recurseSource(mrk); - writeStr("\n"); - if (targets.containsKey(mrk.getAttributeValue("mid"))) { - // write new target - Element tmrk = targets.get(mrk.getAttributeValue("mid")); - writeStr(""); - recurseTarget(tmrk); - writeStr("\n"); - } - writeStr("\n"); - } - } - } - } else { - // is null - if (root.getAttributeValue("translate", "yes").equals("no")) { - writeStr(root.toString()); - } else { - throw new IOException("Segmentation problem found."); - } - } - } else { - // not in - List children = root.getChildren(); - Iterator it = children.iterator(); - while (it.hasNext()) { - recurse(it.next()); - } - } - } - - private static boolean containsSrcText(Element child) { - List list = child.getContent(); - Iterator it = list.iterator(); - while (it.hasNext()) { - XMLNode n = it.next(); - if (n.getNodeType() == XMLNode.TEXT_NODE) { - if (!n.toString().trim().isEmpty()) { - return true; - } - } else if (n.getNodeType() == XMLNode.ELEMENT_NODE) { - Element e = (Element) n; - if (e.getName().equals("g") && containsSrcText(e)) { - return true; - } - } - } - return false; - } - - private static void recurseTarget(Element mrk) throws IOException { - List tnodes = mrk.getContent(); - Iterator tnt = tnodes.iterator(); - while (tnt.hasNext()) { - XMLNode n = tnt.next(); - if (n.getNodeType() == XMLNode.TEXT_NODE) { - writeStr(n.toString()); - } - if (n.getNodeType() == XMLNode.ELEMENT_NODE) { - Element e = (Element) n; - if (e.getName().equals("mrk")) { - recurseTarget(e); - } else { - writeStr(e.toString()); - } - } - } - - } - - private static void recurseSource(Element mrk) throws IOException { - List nodes = mrk.getContent(); - Iterator nt = nodes.iterator(); - while (nt.hasNext()) { - XMLNode n = nt.next(); - if (n.getNodeType() == XMLNode.TEXT_NODE) { - writeStr(n.toString()); - } - if (n.getNodeType() == XMLNode.ELEMENT_NODE) { - Element e = (Element) n; - if (e.getName().equals("mrk")) { - recurseSource(e); - } else { - writeStr(e.toString()); - } - } - } - - } - - private static List getSegments(Element e) { - List result = new Vector<>(); - List children = e.getChildren(); - Iterator it = children.iterator(); - while (it.hasNext()) { - Element child = it.next(); - if (child.getName().equals("mrk") && child.getAttributeValue("mtype", "").equals("seg")) { - result.add(child); - } else { - result.addAll(getSegments(child)); - } - } - return result; - } - - private static boolean containsText(Element source) { - List nodes = source.getContent(); - Iterator it = nodes.iterator(); - while (it.hasNext()) { - XMLNode n = it.next(); - if (n.getNodeType() == XMLNode.TEXT_NODE && !n.toString().trim().isEmpty()) { - return true; - } - if (n.getNodeType() == XMLNode.ELEMENT_NODE) { - Element e = (Element) n; - if (e.getName().equals("mrk") && containsText(e)) { - return true; - } - if (e.getName().equals("g") && containsText(e)) { - return true; - } - } - } - return false; - } - - private static void writeStr(String string) throws IOException { - out.write(string.getBytes(StandardCharsets.UTF_8)); - } -} +/******************************************************************************* + * Copyright (c) 2003-2019 Maxprograms. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/org/documents/epl-v10.html + * + * Contributors: + * Maxprograms - initial API and implementation + *******************************************************************************/ +package com.maxprograms.converters.sdlxliff; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Vector; +import java.lang.System.Logger.Level; +import java.lang.System.Logger; + +import javax.xml.parsers.ParserConfigurationException; + +import org.xml.sax.SAXException; + +import com.maxprograms.converters.Constants; +import com.maxprograms.converters.Utils; +import com.maxprograms.xml.Catalog; +import com.maxprograms.xml.Document; +import com.maxprograms.xml.Element; +import com.maxprograms.xml.SAXBuilder; +import com.maxprograms.xml.XMLNode; +import com.maxprograms.xml.XMLOutputter; + +public class Sdl2Xliff { + + private static FileOutputStream out; + + private Sdl2Xliff() { + // do not instantiate this class + // use run method instead + } + + public static Vector run(Hashtable params) { + Vector result = new Vector<>(); + + try { + String original = params.get("source"); + String output = params.get("xliff"); + String skeletonFile = params.get("skeleton"); + String sourceLanguage = params.get("srcLang"); + String targetLanguage = params.get("tgtLang"); + String srxRules = params.get("srxFile"); + String catalog = params.get("catalog"); + String tgtLang = ""; + if (targetLanguage != null) { + tgtLang = "\" target-language=\"" + targetLanguage; + } + + XliffModel model = new XliffModel(original, srxRules, catalog); + if (model.wasModified()) { + File f = File.createTempFile("temp", ".sdlxliff"); + f.deleteOnExit(); + model.save(f.getAbsolutePath()); + original = f.getAbsolutePath(); + } + model = null; + + SAXBuilder builder = new SAXBuilder(); + builder.setEntityResolver(new Catalog(catalog)); + Document doc = builder.build(original); + + out = new FileOutputStream(output); + writeStr("\n"); + writeStr("\n"); + writeStr("\n"); + writeStr("\n"); + + writeStr("
\n"); + writeStr("\n"); + writeStr("\n"); + writeStr("\n"); + writeStr("
\n"); + writeStr("\n"); + + recurse(doc.getRootElement()); + + writeStr("\n"); + writeStr("
\n"); + writeStr("
\n"); + out.close(); + + XMLOutputter outputter = new XMLOutputter(); + outputter.preserveSpace(true); + outputter.setSkipLinefeed(true); + outputter.writeBOM(true); + try (FileOutputStream skl = new FileOutputStream(skeletonFile)) { + outputter.output(doc, skl); + } + result.add(Constants.SUCCESS); + } catch (IOException | SAXException | ParserConfigurationException e) { + Logger logger = System.getLogger(Sdl2Xliff.class.getName()); + logger.log(Level.ERROR, "Error converting SDLXLIFF file", e); + result.add(Constants.ERROR); + result.add(e.getMessage()); + } + return result; + } + + private static void recurse(Element root) throws IOException { + if (root.getName().equals("trans-unit")) { + if (root.getAttributeValue("translate", "yes").equals("no") + && !(root.getAttributeValue("sdl:locktype", "").equals("Manual") + && containsSrcText(root.getChild("source")))) { + return; + } + Element segSource = root.getChild("seg-source"); + Element target = root.getChild("target"); + if (segSource != null) { + if (containsText(segSource)) { + Hashtable targets = new Hashtable<>(); + if (target != null) { + List tmarks = getSegments(target); + Iterator tt = tmarks.iterator(); + while (tt.hasNext()) { + Element mrk = tt.next(); + targets.put(mrk.getAttributeValue("mid"), mrk); + } + } + List mrks = getSegments(segSource); + if (!mrks.isEmpty()) { + Iterator it = mrks.iterator(); + while (it.hasNext()) { + Element mrk = it.next(); + writeStr("\n"); + // write new source + writeStr(""); + recurseSource(mrk); + writeStr("\n"); + if (targets.containsKey(mrk.getAttributeValue("mid"))) { + // write new target + Element tmrk = targets.get(mrk.getAttributeValue("mid")); + writeStr(""); + recurseTarget(tmrk); + writeStr("\n"); + } + writeStr("\n"); + } + } + } + } else { + // is null + if (root.getAttributeValue("translate", "yes").equals("no")) { + writeStr(root.toString()); + } else { + throw new IOException("Segmentation problem found."); + } + } + } else { + // not in + List children = root.getChildren(); + Iterator it = children.iterator(); + while (it.hasNext()) { + recurse(it.next()); + } + } + } + + private static boolean containsSrcText(Element child) { + List list = child.getContent(); + Iterator it = list.iterator(); + while (it.hasNext()) { + XMLNode n = it.next(); + if (n.getNodeType() == XMLNode.TEXT_NODE) { + if (!n.toString().trim().isEmpty()) { + return true; + } + } else if (n.getNodeType() == XMLNode.ELEMENT_NODE) { + Element e = (Element) n; + if (e.getName().equals("g") && containsSrcText(e)) { + return true; + } + } + } + return false; + } + + private static void recurseTarget(Element mrk) throws IOException { + List tnodes = mrk.getContent(); + Iterator tnt = tnodes.iterator(); + while (tnt.hasNext()) { + XMLNode n = tnt.next(); + if (n.getNodeType() == XMLNode.TEXT_NODE) { + writeStr(n.toString()); + } + if (n.getNodeType() == XMLNode.ELEMENT_NODE) { + Element e = (Element) n; + if (e.getName().equals("mrk")) { + recurseTarget(e); + } else { + writeStr(e.toString()); + } + } + } + + } + + private static void recurseSource(Element mrk) throws IOException { + List nodes = mrk.getContent(); + Iterator nt = nodes.iterator(); + while (nt.hasNext()) { + XMLNode n = nt.next(); + if (n.getNodeType() == XMLNode.TEXT_NODE) { + writeStr(n.toString()); + } + if (n.getNodeType() == XMLNode.ELEMENT_NODE) { + Element e = (Element) n; + if (e.getName().equals("mrk")) { + recurseSource(e); + } else { + writeStr(e.toString()); + } + } + } + + } + + private static List getSegments(Element e) { + List result = new Vector<>(); + List children = e.getChildren(); + Iterator it = children.iterator(); + while (it.hasNext()) { + Element child = it.next(); + if (child.getName().equals("mrk") && child.getAttributeValue("mtype", "").equals("seg")) { + result.add(child); + } else { + result.addAll(getSegments(child)); + } + } + return result; + } + + private static boolean containsText(Element source) { + List nodes = source.getContent(); + Iterator it = nodes.iterator(); + while (it.hasNext()) { + XMLNode n = it.next(); + if (n.getNodeType() == XMLNode.TEXT_NODE && !n.toString().trim().isEmpty()) { + return true; + } + if (n.getNodeType() == XMLNode.ELEMENT_NODE) { + Element e = (Element) n; + if (e.getName().equals("mrk") && containsText(e)) { + return true; + } + if (e.getName().equals("g") && containsText(e)) { + return true; + } + } + } + return false; + } + + private static void writeStr(String string) throws IOException { + out.write(string.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/src/com/maxprograms/converters/sdlxliff/Xliff2Sdl.java b/src/com/maxprograms/converters/sdlxliff/Xliff2Sdl.java index 1034ea39..ec39461a 100644 --- a/src/com/maxprograms/converters/sdlxliff/Xliff2Sdl.java +++ b/src/com/maxprograms/converters/sdlxliff/Xliff2Sdl.java @@ -1,246 +1,247 @@ -/******************************************************************************* - * Copyright (c) 2003-2019 Maxprograms. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 1.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/org/documents/epl-v10.html - * - * Contributors: - * Maxprograms - initial API and implementation - *******************************************************************************/ -package com.maxprograms.converters.sdlxliff; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.Enumeration; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.List; -import java.util.Vector; -import java.lang.System.Logger.Level; -import java.lang.System.Logger; - -import javax.xml.parsers.ParserConfigurationException; - -import org.xml.sax.SAXException; - -import com.maxprograms.converters.UnexistentSegmentException; -import com.maxprograms.xml.Catalog; -import com.maxprograms.xml.Document; -import com.maxprograms.xml.Element; -import com.maxprograms.xml.SAXBuilder; -import com.maxprograms.xml.XMLNode; -import com.maxprograms.xml.XMLOutputter; - -public class Xliff2Sdl { - - private static String sklFile; - private static String xliffFile; - private static Hashtable segments; - private static Document doc; - private static Element root; - private static String catalog; - - private Xliff2Sdl() { - // do not instantiate this class - // use run method instead - } - - public static Vector run(Hashtable params) { - Vector result = new Vector<>(); - - sklFile = params.get("skeleton"); - xliffFile = params.get("xliff"); - catalog = params.get("catalog"); - - String outputFile = params.get("backfile"); - String type = params.get("type"); - if (type == null) { - type = "sdlxliff"; - } - - try { - loadSegments(); - loadSkeleton(); - - Enumeration keys = segments.keys(); - while (keys.hasMoreElements()) { - String key = keys.nextElement(); - Element unit = segments.get(key); - if (type.equals("sdlxliff")) { - if (!unit.getAttributeValue("translate", "yes").equals("no")) { - String fullid = unit.getAttributeValue("id"); - String id = fullid.substring(0, fullid.lastIndexOf('|')); - String mrk = fullid.substring(fullid.lastIndexOf('|') + 1); - replaceTarget(id, mrk, unit.getChild("target"), - unit.getAttributeValue("approved", "no").equals("yes")); - } - } else { - String fullid = unit.getAttributeValue("id"); - if (fullid.indexOf('|') != -1) { - String id = fullid.substring(0, fullid.lastIndexOf('|')); - String mrk = fullid.substring(fullid.lastIndexOf('|') + 1); - replaceTarget(id, mrk, unit.getChild("target"), false); - } - } - } - XMLOutputter outputter = new XMLOutputter(); - outputter.preserveSpace(true); - if (type.equals("sdlxliff")) { - outputter.setSkipLinefeed(true); - outputter.writeBOM(true); - } - File f = new File(outputFile); - File p = f.getParentFile(); - if (p == null) { - p = new File(System.getProperty("user.dir")); - } - if (!p.exists()) { - p.mkdirs(); - } - try (FileOutputStream out = new FileOutputStream(f)) { - outputter.output(doc, out); - } - result.add("0"); - } catch (IOException | SAXException | ParserConfigurationException | UnexistentSegmentException e) { - Logger logger = System.getLogger(Xliff2Sdl.class.getName()); - logger.log(Level.ERROR, "Error merging SDLXLIFF file", e); - result.add("1"); - result.add(e.getMessage()); - } - return result; - } - - private static void replaceTarget(String id, String mrkId, Element translated, boolean approved) - throws UnexistentSegmentException { - if (translated == null) { - return; - } - Element unit = locateUnit(root, id); - if (unit == null) { - throw new UnexistentSegmentException("Missing segment " + id); - } - Element target = unit.getChild("target"); - if (target == null) { - target = new Element("target"); - addTarget(unit, target); - } - Element mrk = locateMrk(target, mrkId); - if (mrk == null) { - mrk = new Element("mrk"); - mrk.setAttribute("mtype", "seg"); - mrk.setAttribute("mid", mrkId); - target.addContent(mrk); - } - mrk.setContent(new Vector<>()); - List content = translated.getContent(); - for (int i = 0; i < content.size(); i++) { - XMLNode n = content.get(i); - if (n.getNodeType() == XMLNode.TEXT_NODE) { - mrk.addContent(n); - } - if (n.getNodeType() == XMLNode.ELEMENT_NODE) { - Element e = new Element(); - e.clone((Element) n); - if (e.getName().equals("x") && e.getAttributeValue("id").startsWith("locked")) { - String lockedTU = e.getAttributeValue("xid"); - Element tu = segments.get(lockedTU); - Element referenced = tu.getChild("source").getChild("g"); - Element g = new Element("g"); - g.setAttribute("id", referenced.getAttributeValue("id")); - mrk.addContent(g); - } else { - mrk.addContent(e); - } - } - } - if (approved) { - Element defs = unit.getChild("sdl:seg-defs"); - if (defs != null) { - List children = defs.getChildren("sdl:seg"); - Iterator it = children.iterator(); - while (it.hasNext()) { - Element seg = it.next(); - if (seg.getAttributeValue("id").equals(mrkId)) { - seg.setAttribute("conf", "Translated"); - } - } - } - } - } - - private static void addTarget(Element unit, Element target) { - List content = unit.getContent(); - List newContent = new Vector<>(); - Iterator it = content.iterator(); - while (it.hasNext()) { - XMLNode node = it.next(); - newContent.add(node); - if (node.getNodeType() == XMLNode.ELEMENT_NODE) { - Element n = (Element) node; - if (n.getName().equals("seg-source")) { - newContent.add(target); - } - } - } - unit.setContent(newContent); - } - - private static Element locateMrk(Element e, String mrkId) { - if (e.getName().equals("mrk") && e.getAttributeValue("mid").equals(mrkId)) { - return e; - } - List children = e.getChildren(); - Iterator it = children.iterator(); - while (it.hasNext()) { - Element res = locateMrk(it.next(), mrkId); - if (res != null) { - return res; - } - } - return null; - } - - private static Element locateUnit(Element e, String id) { - if (e.getName().equals("trans-unit") && e.getAttributeValue("id").equals(id)) { - return e; - } - List children = e.getChildren(); - Iterator it = children.iterator(); - while (it.hasNext()) { - Element res = locateUnit(it.next(), id); - if (res != null) { - return res; - } - } - return null; - } - - private static void loadSkeleton() throws SAXException, IOException, ParserConfigurationException { - SAXBuilder builder = new SAXBuilder(); - builder.setEntityResolver(new Catalog(catalog)); - doc = builder.build(sklFile); - root = doc.getRootElement(); - } - - private static void loadSegments() throws SAXException, IOException, ParserConfigurationException { - SAXBuilder builder = new SAXBuilder(); - builder.setEntityResolver(new Catalog(catalog)); - - Document xdoc = builder.build(xliffFile); - Element xroot = xdoc.getRootElement(); - Element body = xroot.getChild("file").getChild("body"); - List units = body.getChildren("trans-unit"); - Iterator i = units.iterator(); - - segments = new Hashtable<>(); - - while (i.hasNext()) { - Element unit = i.next(); - segments.put(unit.getAttributeValue("id"), unit); - } - } - -} +/******************************************************************************* + * Copyright (c) 2003-2019 Maxprograms. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/org/documents/epl-v10.html + * + * Contributors: + * Maxprograms - initial API and implementation + *******************************************************************************/ +package com.maxprograms.converters.sdlxliff; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Vector; +import java.lang.System.Logger.Level; +import java.lang.System.Logger; + +import javax.xml.parsers.ParserConfigurationException; + +import org.xml.sax.SAXException; + +import com.maxprograms.converters.Constants; +import com.maxprograms.converters.UnexistentSegmentException; +import com.maxprograms.xml.Catalog; +import com.maxprograms.xml.Document; +import com.maxprograms.xml.Element; +import com.maxprograms.xml.SAXBuilder; +import com.maxprograms.xml.XMLNode; +import com.maxprograms.xml.XMLOutputter; + +public class Xliff2Sdl { + + private static String sklFile; + private static String xliffFile; + private static Hashtable segments; + private static Document doc; + private static Element root; + private static String catalog; + + private Xliff2Sdl() { + // do not instantiate this class + // use run method instead + } + + public static Vector run(Hashtable params) { + Vector result = new Vector<>(); + + sklFile = params.get("skeleton"); + xliffFile = params.get("xliff"); + catalog = params.get("catalog"); + + String outputFile = params.get("backfile"); + String type = params.get("type"); + if (type == null) { + type = "sdlxliff"; + } + + try { + loadSegments(); + loadSkeleton(); + + Enumeration keys = segments.keys(); + while (keys.hasMoreElements()) { + String key = keys.nextElement(); + Element unit = segments.get(key); + if (type.equals("sdlxliff")) { + if (!unit.getAttributeValue("translate", "yes").equals("no")) { + String fullid = unit.getAttributeValue("id"); + String id = fullid.substring(0, fullid.lastIndexOf('|')); + String mrk = fullid.substring(fullid.lastIndexOf('|') + 1); + replaceTarget(id, mrk, unit.getChild("target"), + unit.getAttributeValue("approved", "no").equals("yes")); + } + } else { + String fullid = unit.getAttributeValue("id"); + if (fullid.indexOf('|') != -1) { + String id = fullid.substring(0, fullid.lastIndexOf('|')); + String mrk = fullid.substring(fullid.lastIndexOf('|') + 1); + replaceTarget(id, mrk, unit.getChild("target"), false); + } + } + } + XMLOutputter outputter = new XMLOutputter(); + outputter.preserveSpace(true); + if (type.equals("sdlxliff")) { + outputter.setSkipLinefeed(true); + outputter.writeBOM(true); + } + File f = new File(outputFile); + File p = f.getParentFile(); + if (p == null) { + p = new File(System.getProperty("user.dir")); + } + if (!p.exists()) { + p.mkdirs(); + } + try (FileOutputStream out = new FileOutputStream(f)) { + outputter.output(doc, out); + } + result.add(Constants.SUCCESS); + } catch (IOException | SAXException | ParserConfigurationException | UnexistentSegmentException e) { + Logger logger = System.getLogger(Xliff2Sdl.class.getName()); + logger.log(Level.ERROR, "Error merging SDLXLIFF file", e); + result.add(Constants.ERROR); + result.add(e.getMessage()); + } + return result; + } + + private static void replaceTarget(String id, String mrkId, Element translated, boolean approved) + throws UnexistentSegmentException { + if (translated == null) { + return; + } + Element unit = locateUnit(root, id); + if (unit == null) { + throw new UnexistentSegmentException("Missing segment " + id); + } + Element target = unit.getChild("target"); + if (target == null) { + target = new Element("target"); + addTarget(unit, target); + } + Element mrk = locateMrk(target, mrkId); + if (mrk == null) { + mrk = new Element("mrk"); + mrk.setAttribute("mtype", "seg"); + mrk.setAttribute("mid", mrkId); + target.addContent(mrk); + } + mrk.setContent(new Vector<>()); + List content = translated.getContent(); + for (int i = 0; i < content.size(); i++) { + XMLNode n = content.get(i); + if (n.getNodeType() == XMLNode.TEXT_NODE) { + mrk.addContent(n); + } + if (n.getNodeType() == XMLNode.ELEMENT_NODE) { + Element e = new Element(); + e.clone((Element) n); + if (e.getName().equals("x") && e.getAttributeValue("id").startsWith("locked")) { + String lockedTU = e.getAttributeValue("xid"); + Element tu = segments.get(lockedTU); + Element referenced = tu.getChild("source").getChild("g"); + Element g = new Element("g"); + g.setAttribute("id", referenced.getAttributeValue("id")); + mrk.addContent(g); + } else { + mrk.addContent(e); + } + } + } + if (approved) { + Element defs = unit.getChild("sdl:seg-defs"); + if (defs != null) { + List children = defs.getChildren("sdl:seg"); + Iterator it = children.iterator(); + while (it.hasNext()) { + Element seg = it.next(); + if (seg.getAttributeValue("id").equals(mrkId)) { + seg.setAttribute("conf", "Translated"); + } + } + } + } + } + + private static void addTarget(Element unit, Element target) { + List content = unit.getContent(); + List newContent = new Vector<>(); + Iterator it = content.iterator(); + while (it.hasNext()) { + XMLNode node = it.next(); + newContent.add(node); + if (node.getNodeType() == XMLNode.ELEMENT_NODE) { + Element n = (Element) node; + if (n.getName().equals("seg-source")) { + newContent.add(target); + } + } + } + unit.setContent(newContent); + } + + private static Element locateMrk(Element e, String mrkId) { + if (e.getName().equals("mrk") && e.getAttributeValue("mid").equals(mrkId)) { + return e; + } + List children = e.getChildren(); + Iterator it = children.iterator(); + while (it.hasNext()) { + Element res = locateMrk(it.next(), mrkId); + if (res != null) { + return res; + } + } + return null; + } + + private static Element locateUnit(Element e, String id) { + if (e.getName().equals("trans-unit") && e.getAttributeValue("id").equals(id)) { + return e; + } + List children = e.getChildren(); + Iterator it = children.iterator(); + while (it.hasNext()) { + Element res = locateUnit(it.next(), id); + if (res != null) { + return res; + } + } + return null; + } + + private static void loadSkeleton() throws SAXException, IOException, ParserConfigurationException { + SAXBuilder builder = new SAXBuilder(); + builder.setEntityResolver(new Catalog(catalog)); + doc = builder.build(sklFile); + root = doc.getRootElement(); + } + + private static void loadSegments() throws SAXException, IOException, ParserConfigurationException { + SAXBuilder builder = new SAXBuilder(); + builder.setEntityResolver(new Catalog(catalog)); + + Document xdoc = builder.build(xliffFile); + Element xroot = xdoc.getRootElement(); + Element body = xroot.getChild("file").getChild("body"); + List units = body.getChildren("trans-unit"); + Iterator i = units.iterator(); + + segments = new Hashtable<>(); + + while (i.hasNext()) { + Element unit = i.next(); + segments.put(unit.getAttributeValue("id"), unit); + } + } + +} diff --git a/src/com/maxprograms/converters/ts/Ts2Xliff.java b/src/com/maxprograms/converters/ts/Ts2Xliff.java index 906a6e6c..bc5a3e6c 100644 --- a/src/com/maxprograms/converters/ts/Ts2Xliff.java +++ b/src/com/maxprograms/converters/ts/Ts2Xliff.java @@ -1,162 +1,163 @@ -/******************************************************************************* - * Copyright (c) 2003-2019 Maxprograms. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 1.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/org/documents/epl-v10.html - * - * Contributors: - * Maxprograms - initial API and implementation - *******************************************************************************/ -package com.maxprograms.converters.ts; - -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.List; -import java.util.Vector; -import java.lang.System.Logger.Level; -import java.lang.System.Logger; - -import javax.xml.parsers.ParserConfigurationException; - -import org.xml.sax.SAXException; - -import com.maxprograms.converters.Utils; -import com.maxprograms.xml.Document; -import com.maxprograms.xml.Element; -import com.maxprograms.xml.SAXBuilder; -import com.maxprograms.xml.XMLNode; -import com.maxprograms.xml.XMLOutputter; - -public class Ts2Xliff { - private static String sourceLanguage; - private static String targetLanguage; - private static String skeletonFile; - private static String xliffFile; - private static String inputFile; - private static int segId; - private static FileOutputStream output; - private static Document doc; - - private Ts2Xliff() { - // do not instantiate this class - // use run method instead - } - - public static Vector run(Hashtable params) { - Vector result = new Vector<>(); - segId = 0; - inputFile = params.get("source"); - xliffFile = params.get("xliff"); - skeletonFile = params.get("skeleton"); - sourceLanguage = params.get("srcLang"); - targetLanguage = params.get("tgtLang"); - String srcEncoding = params.get("srcEncoding"); - String tgtLang = ""; - if (targetLanguage != null) { - tgtLang = "\" target-language=\"" + targetLanguage; - } - - try { - output = new FileOutputStream(xliffFile); - writeString("\n"); - writeString("\n"); - writeString("\n"); - - writeString("\n"); - writeString("
\n"); - writeString(" \n"); - writeString(" \n"); - writeString(" \n"); - writeString("
\n"); - writeString("\n"); - - SAXBuilder builder = new SAXBuilder(); - doc = builder.build(inputFile); - recurse(doc.getRootElement()); - - try (FileOutputStream skl = new FileOutputStream(skeletonFile)) { - XMLOutputter outputter = new XMLOutputter(); - outputter.preserveSpace(true); - outputter.output(doc, skl); - } - - writeString("\n"); - writeString("
\n"); - writeString("
"); - output.close(); - - result.add("0"); // success - } catch (IOException | SAXException | ParserConfigurationException e) { - Logger logger = System.getLogger(Ts2Xliff.class.getName()); - logger.log(Level.ERROR, "Error converting .ts file", e); - result.add("1"); - result.add(e.getMessage()); - } - - return result; - } - - private static void recurse(Element e) throws IOException { - if (e.getName().equals("message")) { - Element source = e.getChild("source"); - Element target = e.getChild("translation"); - target.setAttribute("id", "" + segId); - String targetText = getTarget(target); - Element comment = e.getChild("comment"); - String approved = (target.getAttributeValue("type", "").equals("") && !targetText.trim().equals("")) ? "yes" //$NON-NLS-5$ - : "no"; //$NON-NLS-1$ - writeString("\n"); - writeString("" + getText(source) + "\n"); - writeString("" + targetText + "\n"); - if (comment != null) { - writeString("" + getText(comment) + "\n"); - } - writeString("\n"); - } else { - List children = e.getChildren(); - Iterator it = children.iterator(); - while (it.hasNext()) { - recurse(it.next()); - } - } - } - - private static String getTarget(Element target) { - List numerusforms = target.getChildren("numerusform"); - if (numerusforms.size() > 0) { - return getText(numerusforms.get(0)); - } - return getText(target); - } - - private static String getText(Element e) { - String result = ""; - int id = 0; - List nodes = e.getContent(); - Iterator it = nodes.iterator(); - while (it.hasNext()) { - XMLNode n = it.next(); - if (n.getNodeType() == XMLNode.TEXT_NODE) { - result = result + n.toString(); - } - if (n.getNodeType() == XMLNode.ELEMENT_NODE) { - Element by = (Element) n; - result = result + "" + Utils.cleanString(by.toString()) + ""; - } - } - return result; - } - - private static void writeString(String string) throws IOException { - output.write(string.getBytes(StandardCharsets.UTF_8)); - } - -} +/******************************************************************************* + * Copyright (c) 2003-2019 Maxprograms. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/org/documents/epl-v10.html + * + * Contributors: + * Maxprograms - initial API and implementation + *******************************************************************************/ +package com.maxprograms.converters.ts; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Vector; +import java.lang.System.Logger.Level; +import java.lang.System.Logger; + +import javax.xml.parsers.ParserConfigurationException; + +import org.xml.sax.SAXException; + +import com.maxprograms.converters.Constants; +import com.maxprograms.converters.Utils; +import com.maxprograms.xml.Document; +import com.maxprograms.xml.Element; +import com.maxprograms.xml.SAXBuilder; +import com.maxprograms.xml.XMLNode; +import com.maxprograms.xml.XMLOutputter; + +public class Ts2Xliff { + private static String sourceLanguage; + private static String targetLanguage; + private static String skeletonFile; + private static String xliffFile; + private static String inputFile; + private static int segId; + private static FileOutputStream output; + private static Document doc; + + private Ts2Xliff() { + // do not instantiate this class + // use run method instead + } + + public static Vector run(Hashtable params) { + Vector result = new Vector<>(); + segId = 0; + inputFile = params.get("source"); + xliffFile = params.get("xliff"); + skeletonFile = params.get("skeleton"); + sourceLanguage = params.get("srcLang"); + targetLanguage = params.get("tgtLang"); + String srcEncoding = params.get("srcEncoding"); + String tgtLang = ""; + if (targetLanguage != null) { + tgtLang = "\" target-language=\"" + targetLanguage; + } + + try { + output = new FileOutputStream(xliffFile); + writeString("\n"); + writeString("\n"); + writeString("\n"); + + writeString("\n"); + writeString("
\n"); + writeString(" \n"); + writeString(" \n"); + writeString(" \n"); + writeString("
\n"); + writeString("\n"); + + SAXBuilder builder = new SAXBuilder(); + doc = builder.build(inputFile); + recurse(doc.getRootElement()); + + try (FileOutputStream skl = new FileOutputStream(skeletonFile)) { + XMLOutputter outputter = new XMLOutputter(); + outputter.preserveSpace(true); + outputter.output(doc, skl); + } + + writeString("\n"); + writeString("
\n"); + writeString("
"); + output.close(); + + result.add(Constants.SUCCESS); + } catch (IOException | SAXException | ParserConfigurationException e) { + Logger logger = System.getLogger(Ts2Xliff.class.getName()); + logger.log(Level.ERROR, "Error converting .ts file", e); + result.add(Constants.ERROR); + result.add(e.getMessage()); + } + + return result; + } + + private static void recurse(Element e) throws IOException { + if (e.getName().equals("message")) { + Element source = e.getChild("source"); + Element target = e.getChild("translation"); + target.setAttribute("id", "" + segId); + String targetText = getTarget(target); + Element comment = e.getChild("comment"); + String approved = (target.getAttributeValue("type", "").equals("") && !targetText.trim().equals("")) ? "yes" //$NON-NLS-5$ + : "no"; //$NON-NLS-1$ + writeString("\n"); + writeString("" + getText(source) + "\n"); + writeString("" + targetText + "\n"); + if (comment != null) { + writeString("" + getText(comment) + "\n"); + } + writeString("\n"); + } else { + List children = e.getChildren(); + Iterator it = children.iterator(); + while (it.hasNext()) { + recurse(it.next()); + } + } + } + + private static String getTarget(Element target) { + List numerusforms = target.getChildren("numerusform"); + if (numerusforms.size() > 0) { + return getText(numerusforms.get(0)); + } + return getText(target); + } + + private static String getText(Element e) { + String result = ""; + int id = 0; + List nodes = e.getContent(); + Iterator it = nodes.iterator(); + while (it.hasNext()) { + XMLNode n = it.next(); + if (n.getNodeType() == XMLNode.TEXT_NODE) { + result = result + n.toString(); + } + if (n.getNodeType() == XMLNode.ELEMENT_NODE) { + Element by = (Element) n; + result = result + "" + Utils.cleanString(by.toString()) + ""; + } + } + return result; + } + + private static void writeString(String string) throws IOException { + output.write(string.getBytes(StandardCharsets.UTF_8)); + } + +} diff --git a/src/com/maxprograms/converters/ts/Xliff2Ts.java b/src/com/maxprograms/converters/ts/Xliff2Ts.java index f897b59e..df519096 100644 --- a/src/com/maxprograms/converters/ts/Xliff2Ts.java +++ b/src/com/maxprograms/converters/ts/Xliff2Ts.java @@ -1,162 +1,163 @@ -/******************************************************************************* - * Copyright (c) 2003-2019 Maxprograms. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 1.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/org/documents/epl-v10.html - * - * Contributors: - * Maxprograms - initial API and implementation - *******************************************************************************/ -package com.maxprograms.converters.ts; - -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.List; -import java.util.Vector; -import java.lang.System.Logger.Level; -import java.lang.System.Logger; - -import javax.xml.parsers.ParserConfigurationException; - -import org.xml.sax.SAXException; - -import com.maxprograms.xml.Document; -import com.maxprograms.xml.Element; -import com.maxprograms.xml.SAXBuilder; -import com.maxprograms.xml.XMLNode; -import com.maxprograms.xml.XMLOutputter; - -public class Xliff2Ts { - - private static Hashtable segments; - private static String xliffFile; - private static Document doc; - - private Xliff2Ts() { - // do not instantiate this class - // use run method instead - } - - public static Vector run(Hashtable params) { - Vector result = new Vector<>(); - try { - xliffFile = params.get("xliff"); - loadSegments(); - - String sklFile = params.get("skeleton"); - SAXBuilder builder = new SAXBuilder(); - doc = builder.build(sklFile); - recurseSkl(doc.getRootElement()); - - String outputFile = params.get("backfile"); - File f = new File(outputFile); - File p = f.getParentFile(); - if (p == null) { - p = new File(System.getProperty("user.dir")); - } - if (!p.exists()) { - p.mkdirs(); - } - if (!f.exists()) { - Files.createFile(Paths.get(f.toURI())); - } - try (FileOutputStream output = new FileOutputStream(f)) { - XMLOutputter outputter = new XMLOutputter(); - outputter.setEncoding(StandardCharsets.UTF_8); - outputter.preserveSpace(true); - outputter.escapeQuotes(true); - outputter.setEmptyDoctype(true); - outputter.output(doc, output); - } - result.add("0"); - } catch (IOException | SAXException | ParserConfigurationException e) { - Logger logger = System.getLogger(Xliff2Ts.class.getName()); - logger.log(Level.ERROR, "Error merging TS file.", e); - result.add("1"); - result.add(e.getMessage()); - } - return result; - } - - private static void recurseSkl(Element e) throws SAXException, IOException, ParserConfigurationException { - if (e.getName().equals("message")) { - Element translation = e.getChild("translation"); - String id = translation.getAttributeValue("id"); - boolean wasObsolete = translation.getAttributeValue("type", "").equals("obsolete"); - String old = translation.getText(); - Element segment = segments.get(id); - Element tmp = getTranslation(segment.getChild("target")); - translation.clone(tmp); - if (segment.getAttributeValue("approved").equals("yes")) { - translation.removeAttribute("type"); - } else { - if (wasObsolete) { - if (old.equals(tmp.getText())) { - translation.setAttribute("type", "obsolete"); - } else { - translation.setAttribute("type", "unfinished"); - } - } else { - translation.setAttribute("type", "unfinished"); - } - } - } else { - List children = e.getChildren(); - Iterator it = children.iterator(); - while (it.hasNext()) { - recurseSkl(it.next()); - } - } - } - - private static Element getTranslation(Element e) throws SAXException, IOException, ParserConfigurationException { - String result = ""; - List nodes = e.getContent(); - Iterator it = nodes.iterator(); - while (it.hasNext()) { - XMLNode n = it.next(); - if (n.getNodeType() == XMLNode.TEXT_NODE) { - result = result + n.toString(); - } - if (n.getNodeType() == XMLNode.ELEMENT_NODE) { - Element by = (Element) n; - result = result + by.getText(); - } - } - result = "" + result + ""; - SAXBuilder b = new SAXBuilder(); - Document d = b.build(new ByteArrayInputStream(result.getBytes(StandardCharsets.UTF_8))); - return d.getRootElement(); - } - - private static void loadSegments() throws SAXException, IOException, ParserConfigurationException { - SAXBuilder builder = new SAXBuilder(); - Document sdoc = builder.build(xliffFile); - Element root = sdoc.getRootElement(); - segments = new Hashtable<>(); - recurseXliff(root); - } - - private static void recurseXliff(Element e) { - List list = e.getChildren(); - Iterator i = list.iterator(); - while (i.hasNext()) { - Element u = i.next(); - if (u.getName().equals("trans-unit")) { - segments.put(u.getAttributeValue("id"), u); - } else { - recurseXliff(u); - } - } - } - -} +/******************************************************************************* + * Copyright (c) 2003-2019 Maxprograms. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/org/documents/epl-v10.html + * + * Contributors: + * Maxprograms - initial API and implementation + *******************************************************************************/ +package com.maxprograms.converters.ts; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Vector; +import java.lang.System.Logger.Level; +import java.lang.System.Logger; + +import javax.xml.parsers.ParserConfigurationException; + +import org.xml.sax.SAXException; + +import com.maxprograms.converters.Constants; +import com.maxprograms.xml.Document; +import com.maxprograms.xml.Element; +import com.maxprograms.xml.SAXBuilder; +import com.maxprograms.xml.XMLNode; +import com.maxprograms.xml.XMLOutputter; + +public class Xliff2Ts { + + private static Hashtable segments; + private static String xliffFile; + private static Document doc; + + private Xliff2Ts() { + // do not instantiate this class + // use run method instead + } + + public static Vector run(Hashtable params) { + Vector result = new Vector<>(); + try { + xliffFile = params.get("xliff"); + loadSegments(); + + String sklFile = params.get("skeleton"); + SAXBuilder builder = new SAXBuilder(); + doc = builder.build(sklFile); + recurseSkl(doc.getRootElement()); + + String outputFile = params.get("backfile"); + File f = new File(outputFile); + File p = f.getParentFile(); + if (p == null) { + p = new File(System.getProperty("user.dir")); + } + if (!p.exists()) { + p.mkdirs(); + } + if (!f.exists()) { + Files.createFile(Paths.get(f.toURI())); + } + try (FileOutputStream output = new FileOutputStream(f)) { + XMLOutputter outputter = new XMLOutputter(); + outputter.setEncoding(StandardCharsets.UTF_8); + outputter.preserveSpace(true); + outputter.escapeQuotes(true); + outputter.setEmptyDoctype(true); + outputter.output(doc, output); + } + result.add(Constants.SUCCESS); + } catch (IOException | SAXException | ParserConfigurationException e) { + Logger logger = System.getLogger(Xliff2Ts.class.getName()); + logger.log(Level.ERROR, "Error merging TS file.", e); + result.add(Constants.ERROR); + result.add(e.getMessage()); + } + return result; + } + + private static void recurseSkl(Element e) throws SAXException, IOException, ParserConfigurationException { + if (e.getName().equals("message")) { + Element translation = e.getChild("translation"); + String id = translation.getAttributeValue("id"); + boolean wasObsolete = translation.getAttributeValue("type", "").equals("obsolete"); + String old = translation.getText(); + Element segment = segments.get(id); + Element tmp = getTranslation(segment.getChild("target")); + translation.clone(tmp); + if (segment.getAttributeValue("approved").equals("yes")) { + translation.removeAttribute("type"); + } else { + if (wasObsolete) { + if (old.equals(tmp.getText())) { + translation.setAttribute("type", "obsolete"); + } else { + translation.setAttribute("type", "unfinished"); + } + } else { + translation.setAttribute("type", "unfinished"); + } + } + } else { + List children = e.getChildren(); + Iterator it = children.iterator(); + while (it.hasNext()) { + recurseSkl(it.next()); + } + } + } + + private static Element getTranslation(Element e) throws SAXException, IOException, ParserConfigurationException { + String result = ""; + List nodes = e.getContent(); + Iterator it = nodes.iterator(); + while (it.hasNext()) { + XMLNode n = it.next(); + if (n.getNodeType() == XMLNode.TEXT_NODE) { + result = result + n.toString(); + } + if (n.getNodeType() == XMLNode.ELEMENT_NODE) { + Element by = (Element) n; + result = result + by.getText(); + } + } + result = "" + result + ""; + SAXBuilder b = new SAXBuilder(); + Document d = b.build(new ByteArrayInputStream(result.getBytes(StandardCharsets.UTF_8))); + return d.getRootElement(); + } + + private static void loadSegments() throws SAXException, IOException, ParserConfigurationException { + SAXBuilder builder = new SAXBuilder(); + Document sdoc = builder.build(xliffFile); + Element root = sdoc.getRootElement(); + segments = new Hashtable<>(); + recurseXliff(root); + } + + private static void recurseXliff(Element e) { + List list = e.getChildren(); + Iterator i = list.iterator(); + while (i.hasNext()) { + Element u = i.next(); + if (u.getName().equals("trans-unit")) { + segments.put(u.getAttributeValue("id"), u); + } else { + recurseXliff(u); + } + } + } + +} diff --git a/src/com/maxprograms/converters/txml/Txml2Xliff.java b/src/com/maxprograms/converters/txml/Txml2Xliff.java index 68d277b1..3c0e62b1 100644 --- a/src/com/maxprograms/converters/txml/Txml2Xliff.java +++ b/src/com/maxprograms/converters/txml/Txml2Xliff.java @@ -1,198 +1,199 @@ -/******************************************************************************* - * Copyright (c) 2003-2019 Maxprograms. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 1.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/org/documents/epl-v10.html - * - * Contributors: - * Maxprograms - initial API and implementation - *******************************************************************************/ -package com.maxprograms.converters.txml; - -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.List; -import java.util.Vector; -import java.lang.System.Logger.Level; -import java.lang.System.Logger; - -import javax.xml.parsers.ParserConfigurationException; - -import org.xml.sax.SAXException; - -import com.maxprograms.converters.Utils; -import com.maxprograms.xml.Catalog; -import com.maxprograms.xml.Document; -import com.maxprograms.xml.Element; -import com.maxprograms.xml.SAXBuilder; -import com.maxprograms.xml.XMLNode; -import com.maxprograms.xml.XMLOutputter; - -public class Txml2Xliff { - - private static String inputFile; - private static String xliffFile; - private static String skeletonFile; - private static String sourceLanguage; - private static String targetLanguage; - private static String catalog; - private static FileOutputStream output; - private static int tagId; - private static int segNum; - private static Document doc; - private static String srcEncoding; - - private Txml2Xliff() { - // do not instantiate this class - // use run method instead - } - - public static Vector run(Hashtable params) { - Vector result = new Vector<>(); - inputFile = params.get("source"); - xliffFile = params.get("xliff"); - skeletonFile = params.get("skeleton"); - sourceLanguage = params.get("srcLang"); - targetLanguage = params.get("tgtLang"); - catalog = params.get("catalog"); - srcEncoding = params.get("srcEncoding"); - - try { - SAXBuilder builder = new SAXBuilder(); - builder.setEntityResolver(new Catalog(catalog)); - doc = builder.build(inputFile); - Element root = doc.getRootElement(); - - output = new FileOutputStream(xliffFile); - writeHeader(inputFile, skeletonFile); - recurse(root); - writeEnd(); - output.close(); - - try (FileOutputStream skl = new FileOutputStream(skeletonFile)) { - XMLOutputter outputter = new XMLOutputter(); - outputter.output(doc, skl); - } - - result.add("0"); - } catch (IOException | SAXException | ParserConfigurationException e) { - Logger logger = System.getLogger(Txml2Xliff.class.getName()); - logger.log(Level.ERROR, "Error converting TXML file", e); - result.add("1"); - if (e.getMessage() != null) { - result.add(e.getMessage()); - } else { - result.add("Unknown error"); - } - } - return result; - } - - private static void writeHeader(String source, String skeleton) throws IOException { - String tgtLang = ""; - if (targetLanguage != null) { - tgtLang = "\" target-language=\"" + targetLanguage; - } - - writeStr("\n"); - writeStr( - "\n"); - writeStr("\n"); - writeStr("\n"); - writeStr("
\n"); - writeStr(" \n"); - writeStr(" \n"); - writeStr(" \n"); - writeStr("
\n"); - writeStr("\n"); - } - - private static void recurse(Element root) throws IOException { - List children = root.getChildren(); - Iterator it = children.iterator(); - while (it.hasNext()) { - Element child = it.next(); - if (child.getName().equals("segment") || child.getName().equals("localizable")) { - parseSegment(child); - } else { - recurse(child); - } - } - } - - private static void parseSegment(Element segment) throws IOException { - segNum++; - tagId = 0; - writeStr("\n"); - Element src = segment.getChild("source"); - Element tgt = segment.getChild("target"); - writeStr(parseElement(src) + "\n"); - if (tgt == null) { - tgt = new Element("target"); - segment.addContent(tgt); - } - tagId = 0; - writeStr(parseElement(tgt) + "\n"); - Element comments = segment.getChild("comments"); - if (comments != null) { - parseComments(comments); - } - writeStr("\n"); - } - - private static String parseElement(Element ele) { - StringBuilder result = new StringBuilder(); - result.append("<" + ele.getName() + ">"); - List content = ele.getContent(); - Iterator it = content.iterator(); - while (it.hasNext()) { - XMLNode o = it.next(); - if (o.getNodeType() == XMLNode.TEXT_NODE) { - result.append(o.toString()); - } - if (o.getNodeType() == XMLNode.ELEMENT_NODE) { - Element tag = (Element) o; - result.append(parseTag(tag)); - } - } - result.append(""); - if (ele.getName().equals("target")) { - ele.setContent(new Vector()); - ele.setText("%%%" + segNum + "%%%"); - } - return result.toString(); - } - - private static String parseTag(Element tag) { - tagId++; - return "" + Utils.cleanString(tag.toString()) + ""; - } - - private static void parseComments(Element comments) throws IOException { - List list = comments.getChildren("comment"); - Iterator it = list.iterator(); - while (it.hasNext()) { - Element comment = it.next(); - writeStr(""); - writeStr(Utils.cleanString(comment.getText())); - writeStr("\n"); - } - } - - private static void writeEnd() throws IOException { - writeStr("\n"); - writeStr("
\n"); - writeStr("
\n"); - } - - private static void writeStr(String string) throws IOException { - output.write(string.getBytes(StandardCharsets.UTF_8)); - } - -} +/******************************************************************************* + * Copyright (c) 2003-2019 Maxprograms. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/org/documents/epl-v10.html + * + * Contributors: + * Maxprograms - initial API and implementation + *******************************************************************************/ +package com.maxprograms.converters.txml; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Vector; +import java.lang.System.Logger.Level; +import java.lang.System.Logger; + +import javax.xml.parsers.ParserConfigurationException; + +import org.xml.sax.SAXException; + +import com.maxprograms.converters.Constants; +import com.maxprograms.converters.Utils; +import com.maxprograms.xml.Catalog; +import com.maxprograms.xml.Document; +import com.maxprograms.xml.Element; +import com.maxprograms.xml.SAXBuilder; +import com.maxprograms.xml.XMLNode; +import com.maxprograms.xml.XMLOutputter; + +public class Txml2Xliff { + + private static String inputFile; + private static String xliffFile; + private static String skeletonFile; + private static String sourceLanguage; + private static String targetLanguage; + private static String catalog; + private static FileOutputStream output; + private static int tagId; + private static int segNum; + private static Document doc; + private static String srcEncoding; + + private Txml2Xliff() { + // do not instantiate this class + // use run method instead + } + + public static Vector run(Hashtable params) { + Vector result = new Vector<>(); + inputFile = params.get("source"); + xliffFile = params.get("xliff"); + skeletonFile = params.get("skeleton"); + sourceLanguage = params.get("srcLang"); + targetLanguage = params.get("tgtLang"); + catalog = params.get("catalog"); + srcEncoding = params.get("srcEncoding"); + + try { + SAXBuilder builder = new SAXBuilder(); + builder.setEntityResolver(new Catalog(catalog)); + doc = builder.build(inputFile); + Element root = doc.getRootElement(); + + output = new FileOutputStream(xliffFile); + writeHeader(inputFile, skeletonFile); + recurse(root); + writeEnd(); + output.close(); + + try (FileOutputStream skl = new FileOutputStream(skeletonFile)) { + XMLOutputter outputter = new XMLOutputter(); + outputter.output(doc, skl); + } + + result.add(Constants.SUCCESS); + } catch (IOException | SAXException | ParserConfigurationException e) { + Logger logger = System.getLogger(Txml2Xliff.class.getName()); + logger.log(Level.ERROR, "Error converting TXML file", e); + result.add(Constants.ERROR); + if (e.getMessage() != null) { + result.add(e.getMessage()); + } else { + result.add("Unknown error"); + } + } + return result; + } + + private static void writeHeader(String source, String skeleton) throws IOException { + String tgtLang = ""; + if (targetLanguage != null) { + tgtLang = "\" target-language=\"" + targetLanguage; + } + + writeStr("\n"); + writeStr( + "\n"); + writeStr("\n"); + writeStr("\n"); + writeStr("
\n"); + writeStr(" \n"); + writeStr(" \n"); + writeStr(" \n"); + writeStr("
\n"); + writeStr("\n"); + } + + private static void recurse(Element root) throws IOException { + List children = root.getChildren(); + Iterator it = children.iterator(); + while (it.hasNext()) { + Element child = it.next(); + if (child.getName().equals("segment") || child.getName().equals("localizable")) { + parseSegment(child); + } else { + recurse(child); + } + } + } + + private static void parseSegment(Element segment) throws IOException { + segNum++; + tagId = 0; + writeStr("\n"); + Element src = segment.getChild("source"); + Element tgt = segment.getChild("target"); + writeStr(parseElement(src) + "\n"); + if (tgt == null) { + tgt = new Element("target"); + segment.addContent(tgt); + } + tagId = 0; + writeStr(parseElement(tgt) + "\n"); + Element comments = segment.getChild("comments"); + if (comments != null) { + parseComments(comments); + } + writeStr("\n"); + } + + private static String parseElement(Element ele) { + StringBuilder result = new StringBuilder(); + result.append("<" + ele.getName() + ">"); + List content = ele.getContent(); + Iterator it = content.iterator(); + while (it.hasNext()) { + XMLNode o = it.next(); + if (o.getNodeType() == XMLNode.TEXT_NODE) { + result.append(o.toString()); + } + if (o.getNodeType() == XMLNode.ELEMENT_NODE) { + Element tag = (Element) o; + result.append(parseTag(tag)); + } + } + result.append(""); + if (ele.getName().equals("target")) { + ele.setContent(new Vector()); + ele.setText("%%%" + segNum + "%%%"); + } + return result.toString(); + } + + private static String parseTag(Element tag) { + tagId++; + return "" + Utils.cleanString(tag.toString()) + ""; + } + + private static void parseComments(Element comments) throws IOException { + List list = comments.getChildren("comment"); + Iterator it = list.iterator(); + while (it.hasNext()) { + Element comment = it.next(); + writeStr(""); + writeStr(Utils.cleanString(comment.getText())); + writeStr("\n"); + } + } + + private static void writeEnd() throws IOException { + writeStr("\n"); + writeStr("
\n"); + writeStr("
\n"); + } + + private static void writeStr(String string) throws IOException { + output.write(string.getBytes(StandardCharsets.UTF_8)); + } + +} diff --git a/src/com/maxprograms/converters/txml/Xliff2Txml.java b/src/com/maxprograms/converters/txml/Xliff2Txml.java index 6b2bc88e..9b5e4c6d 100644 --- a/src/com/maxprograms/converters/txml/Xliff2Txml.java +++ b/src/com/maxprograms/converters/txml/Xliff2Txml.java @@ -1,160 +1,161 @@ -/******************************************************************************* - * Copyright (c) 2003-2019 Maxprograms. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 1.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/org/documents/epl-v10.html - * - * Contributors: - * Maxprograms - initial API and implementation - *******************************************************************************/ -package com.maxprograms.converters.txml; - -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.List; -import java.util.Vector; -import java.lang.System.Logger.Level; -import java.lang.System.Logger; - -import javax.xml.parsers.ParserConfigurationException; - -import org.xml.sax.SAXException; - -import com.maxprograms.xml.Catalog; -import com.maxprograms.xml.Document; -import com.maxprograms.xml.Element; -import com.maxprograms.xml.SAXBuilder; -import com.maxprograms.xml.TextNode; -import com.maxprograms.xml.XMLNode; -import com.maxprograms.xml.XMLOutputter; - -public class Xliff2Txml { - - private static String sklFile; - private static String xliffFile; - private static String catalog; - private static String encoding; - private static Hashtable segments; - private static String outputFile; - private static Document doc; - - private Xliff2Txml() { - // do not instantiate this class - // use run method instead - } - - public static Vector run(Hashtable params) { - Vector result = new Vector<>(); - - sklFile = params.get("skeleton"); - xliffFile = params.get("xliff"); - catalog = params.get("catalog"); - encoding = params.get("encoding"); - outputFile = params.get("backfile"); - - try { - loadSegments(); - SAXBuilder builder = new SAXBuilder(); - doc = builder.build(sklFile); - Element root = doc.getRootElement(); - replaceTargets(root); - XMLOutputter outputter = new XMLOutputter(); - outputter.preserveSpace(true); - outputter.setEncoding(Charset.forName(encoding)); - File f = new File(outputFile); - File p = f.getParentFile(); - if (p == null) { - p = new File(System.getProperty("user.dir")); - } - if (!p.exists()) { - p.mkdirs(); - } - if (!f.exists()) { - Files.createFile(Paths.get(f.toURI())); - } - try (FileOutputStream output = new FileOutputStream(f)) { - outputter.output(doc, output); - } - result.add("0"); - } catch (IOException | SAXException | ParserConfigurationException e) { - Logger logger = System.getLogger(Xliff2Txml.class.getName()); - logger.log(Level.ERROR, "Error merging file", e); - result.add("1"); - if (e.getMessage() != null) { - result.add(e.getMessage()); - } else { - result.add("Unknown error"); - } - } - - return result; - } - - private static void replaceTargets(Element e) throws SAXException, IOException, ParserConfigurationException { - if (e.getName().equals("revisions")) { - return; - } - if (e.getName().equals("target")) { - String id = e.getText().substring(3); - id = id.substring(0, id.length() - 3); - Element seg = segments.get(id); - e.setText(""); - List content = seg.getChild("target").getContent(); - Iterator it = content.iterator(); - while (it.hasNext()) { - XMLNode n = it.next(); - if (n.getNodeType() == XMLNode.TEXT_NODE) { - e.addContent(((TextNode) n).getText().replaceAll("[\\n\\r]", "")); - } - if (n.getNodeType() == XMLNode.ELEMENT_NODE) { - Element tag = (Element) n; - e.addContent(buildTag(tag.getText().replaceAll("[\\n\\r]", ""))); - } - } - } else { - List children = e.getChildren(); - Iterator it = children.iterator(); - while (it.hasNext()) { - replaceTargets(it.next()); - } - } - } - - private static Element buildTag(String text) throws SAXException, IOException, ParserConfigurationException { - SAXBuilder builder = new SAXBuilder(); - Document sdoc = builder.build(new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8))); - Element sroot = sdoc.getRootElement(); - Element tag = new Element(sroot.getName()); - tag.clone(sroot); - return tag; - } - - private static void loadSegments() throws SAXException, IOException, ParserConfigurationException { - - SAXBuilder builder = new SAXBuilder(); - builder.setEntityResolver(new Catalog(catalog)); - - Document sdoc = builder.build(xliffFile); - Element root = sdoc.getRootElement(); - Element body = root.getChild("file").getChild("body"); - List units = body.getChildren("trans-unit"); - Iterator i = units.iterator(); - - segments = new Hashtable<>(); - - while (i.hasNext()) { - Element unit = i.next(); - segments.put(unit.getAttributeValue("id"), unit); - } - } -} +/******************************************************************************* + * Copyright (c) 2003-2019 Maxprograms. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/org/documents/epl-v10.html + * + * Contributors: + * Maxprograms - initial API and implementation + *******************************************************************************/ +package com.maxprograms.converters.txml; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Vector; +import java.lang.System.Logger.Level; +import java.lang.System.Logger; + +import javax.xml.parsers.ParserConfigurationException; + +import org.xml.sax.SAXException; + +import com.maxprograms.converters.Constants; +import com.maxprograms.xml.Catalog; +import com.maxprograms.xml.Document; +import com.maxprograms.xml.Element; +import com.maxprograms.xml.SAXBuilder; +import com.maxprograms.xml.TextNode; +import com.maxprograms.xml.XMLNode; +import com.maxprograms.xml.XMLOutputter; + +public class Xliff2Txml { + + private static String sklFile; + private static String xliffFile; + private static String catalog; + private static String encoding; + private static Hashtable segments; + private static String outputFile; + private static Document doc; + + private Xliff2Txml() { + // do not instantiate this class + // use run method instead + } + + public static Vector run(Hashtable params) { + Vector result = new Vector<>(); + + sklFile = params.get("skeleton"); + xliffFile = params.get("xliff"); + catalog = params.get("catalog"); + encoding = params.get("encoding"); + outputFile = params.get("backfile"); + + try { + loadSegments(); + SAXBuilder builder = new SAXBuilder(); + doc = builder.build(sklFile); + Element root = doc.getRootElement(); + replaceTargets(root); + XMLOutputter outputter = new XMLOutputter(); + outputter.preserveSpace(true); + outputter.setEncoding(Charset.forName(encoding)); + File f = new File(outputFile); + File p = f.getParentFile(); + if (p == null) { + p = new File(System.getProperty("user.dir")); + } + if (!p.exists()) { + p.mkdirs(); + } + if (!f.exists()) { + Files.createFile(Paths.get(f.toURI())); + } + try (FileOutputStream output = new FileOutputStream(f)) { + outputter.output(doc, output); + } + result.add(Constants.SUCCESS); + } catch (IOException | SAXException | ParserConfigurationException e) { + Logger logger = System.getLogger(Xliff2Txml.class.getName()); + logger.log(Level.ERROR, "Error merging file", e); + result.add(Constants.ERROR); + if (e.getMessage() != null) { + result.add(e.getMessage()); + } else { + result.add("Unknown error"); + } + } + + return result; + } + + private static void replaceTargets(Element e) throws SAXException, IOException, ParserConfigurationException { + if (e.getName().equals("revisions")) { + return; + } + if (e.getName().equals("target")) { + String id = e.getText().substring(3); + id = id.substring(0, id.length() - 3); + Element seg = segments.get(id); + e.setText(""); + List content = seg.getChild("target").getContent(); + Iterator it = content.iterator(); + while (it.hasNext()) { + XMLNode n = it.next(); + if (n.getNodeType() == XMLNode.TEXT_NODE) { + e.addContent(((TextNode) n).getText().replaceAll("[\\n\\r]", "")); + } + if (n.getNodeType() == XMLNode.ELEMENT_NODE) { + Element tag = (Element) n; + e.addContent(buildTag(tag.getText().replaceAll("[\\n\\r]", ""))); + } + } + } else { + List children = e.getChildren(); + Iterator it = children.iterator(); + while (it.hasNext()) { + replaceTargets(it.next()); + } + } + } + + private static Element buildTag(String text) throws SAXException, IOException, ParserConfigurationException { + SAXBuilder builder = new SAXBuilder(); + Document sdoc = builder.build(new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8))); + Element sroot = sdoc.getRootElement(); + Element tag = new Element(sroot.getName()); + tag.clone(sroot); + return tag; + } + + private static void loadSegments() throws SAXException, IOException, ParserConfigurationException { + + SAXBuilder builder = new SAXBuilder(); + builder.setEntityResolver(new Catalog(catalog)); + + Document sdoc = builder.build(xliffFile); + Element root = sdoc.getRootElement(); + Element body = root.getChild("file").getChild("body"); + List units = body.getChildren("trans-unit"); + Iterator i = units.iterator(); + + segments = new Hashtable<>(); + + while (i.hasNext()) { + Element unit = i.next(); + segments.put(unit.getAttributeValue("id"), unit); + } + } +} diff --git a/src/com/maxprograms/converters/xml/Xliff2Xml.java b/src/com/maxprograms/converters/xml/Xliff2Xml.java index 45ba9183..7f29b43d 100644 --- a/src/com/maxprograms/converters/xml/Xliff2Xml.java +++ b/src/com/maxprograms/converters/xml/Xliff2Xml.java @@ -1,563 +1,564 @@ -/******************************************************************************* - * Copyright (c) 2003-2019 Maxprograms. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 1.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/org/documents/epl-v10.html - * - * Contributors: - * Maxprograms - initial API and implementation - *******************************************************************************/ -package com.maxprograms.converters.xml; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.text.MessageFormat; -import java.util.Enumeration; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.List; -import java.util.StringTokenizer; -import java.util.Vector; -import java.lang.System.Logger.Level; -import java.lang.System.Logger; - -import javax.xml.parsers.ParserConfigurationException; - -import org.xml.sax.SAXException; - -import com.maxprograms.xml.Catalog; -import com.maxprograms.xml.Document; -import com.maxprograms.xml.Element; -import com.maxprograms.xml.SAXBuilder; -import com.maxprograms.xml.TextNode; -import com.maxprograms.xml.XMLNode; -import com.maxprograms.xml.XMLOutputter; - -public class Xliff2Xml { - - private static final Logger LOGGER = System.getLogger(Xliff2Xml.class.getName()); - - private static InputStreamReader input; - private static BufferedReader buffer; - private static String sklFile; - private static String xliffFile; - private static String line; - private static Hashtable segments; - private static FileOutputStream output; - private static String encoding; - private static Catalog catalog; - private static Hashtable entities; - private static boolean inDesign = false; - private static boolean inAttribute; - private static boolean inCData; - private static boolean dita_based = false; - private static boolean IDML; - - private Xliff2Xml() { - // do not instantiate this class - // use run method instead - } - - public static Vector run(Hashtable params) { - - Vector result = new Vector<>(); - - sklFile = params.get("skeleton"); - xliffFile = params.get("xliff"); - encoding = params.get("encoding"); - String isInDesign = params.get("InDesign"); - if (isInDesign != null) { - inDesign = true; - } - String isIDML = params.get("IDML"); - if (isIDML != null) { - IDML = true; - } - String isDitaBased = params.get("dita_based"); - if (isDitaBased != null) { - dita_based = true; - } - try { - catalog = new Catalog(params.get("catalog")); - String outputFile = params.get("backfile"); - File f = new File(outputFile); - File p = f.getParentFile(); - if (p == null) { - p = new File(System.getProperty("user.dir")); - } - if (!p.exists()) { - p.mkdirs(); - } - if (!f.exists()) { - Files.createFile(Paths.get(f.toURI())); - } - output = new FileOutputStream(f); - loadSegments(); - input = new InputStreamReader(new FileInputStream(sklFile), StandardCharsets.UTF_8); - buffer = new BufferedReader(input); - line = buffer.readLine(); - while (line != null) { - line = line + "\n"; - if (line.indexOf("%%%") != -1) { - // - // contains translatable text - // - int index = line.indexOf("%%%"); - while (index != -1) { - String start = line.substring(0, index); - writeString(start); - line = line.substring(index + 3); - String code = line.substring(0, line.indexOf("%%%")); - line = line.substring(line.indexOf("%%%\n") + 4); - Element segment = segments.get(code); - if (segment != null) { - inAttribute = segment.getAttributeValue("restype", "").equals("x-attribute"); - inCData = segment.getAttributeValue("restype", "").equals("x-cdata"); - Element target = segment.getChild("target"); - Element source = segment.getChild("source"); - if (target != null) { - if (segment.getAttributeValue("approved", "no").equals("yes")) { - writeString(extractText(target)); - } else { - writeString(extractText(source)); - } - } else { - writeString(extractText(source)); - } - } else { - result.add("1"); - MessageFormat mf = new MessageFormat("Segment {0} not found."); - result.add(mf.format(new Object[] { code })); - return result; - } - - index = line.indexOf("%%%"); - if (index == -1) { - writeString(line); - } - } // end while - } else { - // - // non translatable portion - // - writeString(line); - } - line = buffer.readLine(); - } - - output.close(); - output = null; - if (dita_based) { - try { - removeTranslate(outputFile); - } catch (SAXException sax) { - LOGGER.log(Level.ERROR, "removeTranslate error: " + outputFile); - throw sax; - } - } - if (inDesign) { - removeSeparators(outputFile); - } - result.add("0"); - } catch (IOException | SAXException | ParserConfigurationException e) { - LOGGER.log(Level.ERROR, "Error merging file", e); - result.add("1"); - result.add(e.getMessage()); - } - return result; - } - - private static void removeTranslate(String outputFile) - throws SAXException, IOException, ParserConfigurationException { - SAXBuilder builder = new SAXBuilder(); - builder.setEntityResolver(catalog); - Document doc = builder.build(outputFile); - Element root = doc.getRootElement(); - removeTranslateAtt(root); - XMLOutputter outputter = new XMLOutputter(); - outputter.preserveSpace(true); - try (FileOutputStream out = new FileOutputStream(outputFile)) { - outputter.output(doc, out); - } - } - - private static void removeTranslateAtt(Element e) { - if (e.getAttributeValue("removeTranslate", "no").equals("yes")) { - e.removeAttribute("translate"); - e.removeAttribute("removeTranslate"); - } - List children = e.getChildren(); - for (Element child : children) { - removeTranslateAtt(child); - } - } - - private static void removeSeparators(String outputFile) - throws SAXException, IOException, ParserConfigurationException { - SAXBuilder builder = new SAXBuilder(); - builder.setEntityResolver(catalog); - Document doc = builder.build(outputFile); - Element root = doc.getRootElement(); - recurse(root); - XMLOutputter outputter = new XMLOutputter(); - outputter.preserveSpace(true); - try (FileOutputStream out = new FileOutputStream(outputFile)) { - outputter.output(doc, out); - } - } - - private static void recurse(Element e) { - List content = e.getContent(); - for (int i = 0; i < content.size(); i++) { - XMLNode n = content.get(i); - if (n.getNodeType() == XMLNode.TEXT_NODE) { - TextNode t = (TextNode) n; - if (t.getText().startsWith("c_")) { - t.setText("c_" + t.getText().substring(2).replaceAll("_", "~sep~")); - } - } - if (n.getNodeType() == XMLNode.ELEMENT_NODE) { - recurse((Element) n); - } - } - } - - private static String extractText(Element element) throws SAXException { - String result = ""; - List content = element.getContent(); - Iterator i = content.iterator(); - - if (element.getName().equals("ph")) { - return fixEntities(element); - } - if (dita_based && element.getName().equals("mrk")) { - return cleanMrk(element); - } - while (i.hasNext()) { - XMLNode n = i.next(); - switch (n.getNodeType()) { - case XMLNode.ELEMENT_NODE: - Element e = (Element) n; - String ph = extractText(e); - result = result + ph; - break; - case XMLNode.TEXT_NODE: - if (inAttribute) { - result = result + addEntities(((TextNode) n).getText()).replaceAll("\"", """); - } else if (inCData) { - result = result + ((TextNode) n).getText(); - } else { - String text = ((TextNode) n).getText(); - if (IDML && text.indexOf('\n') != -1) { - text = text.replaceAll("\\n", ""); - } - result = result + addEntities(text); - } - break; - default: - // ignore - break; - } - } - return result; - } - - private static String cleanMrk(Element element) throws SAXException { - String ts = element.getAttributeValue("ts", ""); - if (ts.isEmpty()) { - throw new SAXException("Broken element."); - } - ts = restoreChars(ts).trim(); - String name = ""; - for (int i = 1; i < ts.length(); i++) { - if (Character.isSpaceChar(ts.charAt(i))) { - break; - } - name = name + ts.charAt(i); - } - String content = ""; - List nodes = element.getContent(); - Iterator it = nodes.iterator(); - while (it.hasNext()) { - XMLNode n = it.next(); - switch (n.getNodeType()) { - case XMLNode.ELEMENT_NODE: - Element e = (Element) n; - String ph = extractText(e); - content = content + ph; - break; - case XMLNode.TEXT_NODE: - content = content + addEntities(((TextNode) n).getText()); - break; - default: - // ignore - break; - } - } - return ts + content + ""; // TODO recurse content - } - - private static String restoreChars(String string) { - String result = string.replaceAll(Xml2Xliff.MATHLT, "<"); - result = result.replaceAll(Xml2Xliff.MATHGT, ">"); - result = result.replaceAll(Xml2Xliff.DOUBLEPRIME, "\""); - result = result.replaceAll(Xml2Xliff.GAMP, "&"); - return result; - } - - public static String fixEntities(Element element) { - String string = element.getText(); - - int start = string.indexOf('&'); - String result = ""; - if (start > 0) { - result = string.substring(0, start); - string = string.substring(start); - } - while (start != -1) { - int colon = string.indexOf(';'); - if (colon == -1) { - // no ";", we are not in an entity - result = result + "&"; - string = string.substring(1); - } else { - boolean inEntity = true; - for (int i = 1; i < colon; i++) { - char c = string.charAt(i); - if (Character.isWhitespace(c) || "&.@$*()[]{},/?\\\"\'+=-^".indexOf(c) != -1) { - inEntity = false; - break; - } - } - if (!inEntity) { - result = result + "&"; - string = string.substring(1); - } else { - result = result + string.substring(0, colon + 1); - string = string.substring(colon + 1); - } - } - start = string.indexOf('&'); - if (start > 0) { - result = result + string.substring(0, start); - string = string.substring(start); - } - } - - return (result + string).replaceAll("###AMP###", "&"); - } - - private static String replaceEntities(String original, String token, String entity) { - String result = original; - int index = result.indexOf(token); - while (index != -1) { - String before = result.substring(0, index); - String after = result.substring(index + token.length()); - // check if we are not inside an entity - int amp = before.lastIndexOf('&'); - if (amp == -1) { - // we are not in an entity - result = before + entity + after; - } else { - boolean inEntity = true; - for (int i = amp; i < before.length(); i++) { - char c = before.charAt(i); - if (Character.isWhitespace(c) || ";.@$*()[]{},/?\\\"\'+=-^".indexOf(c) != -1) { - inEntity = false; - break; - } - } - if (inEntity) { - // check for a colon in "after" - int colon = after.indexOf(';'); - if (colon == -1) { - // we are not in an entity - result = before + entity + after; - } else { - // verify is there is something that breaks the entity before - for (int i = 0; i < colon; i++) { - char c = after.charAt(i); - if (Character.isWhitespace(c) || "&.@$*()[]{},/?\\\"\'+=-^".indexOf(c) != -1) { - break; - } - } - } - } else { - // we are not in an entity - result = before + entity + after; - } - } - if (index < result.length()) { - index = result.indexOf(token, index + 1); - } - } - return result; - } - - private static String addEntities(String string) { - String result = string; - int index = result.indexOf('&'); - while (index != -1) { - int smcolon = result.indexOf(';', index); - if (smcolon == -1) { - // no semicolon. there is no chance it is an entity - result = result.substring(0, index) + "&" + result.substring(index + 1); - index++; - } else { - int space = result.indexOf(' ', index); - if (space == -1) { - String name = result.substring(index + 1, smcolon); - if (entities.containsKey(name)) { - // it is an entity, jump to the semicolon - index = smcolon; - } else { - result = result.substring(0, index) + "&" + result.substring(index + 1); - index++; - } - } else { - // check if space appears before the semicolon - if (space < smcolon) { - // not an entity! - result = result.substring(0, index) + "&" + result.substring(index + 1); - index++; - } else { - String name = result.substring(index + 1, smcolon); - if (entities.containsKey(name)) { - // it is a known entity, jump to the semicolon - index = smcolon; - } else { - result = result.substring(0, index) + "&" + result.substring(index + 1); - index++; - } - } - } - } - if (index < result.length() && index >= 0) { - index = result.indexOf('&', index); - } else { - index = -1; - } - } - StringTokenizer tok = new StringTokenizer(result, "<>", true); - StringBuilder buff = new StringBuilder(); - while (tok.hasMoreElements()) { - String str = tok.nextToken(); - if (str.equals("<")) { - buff.append("<"); - } else if (str.equals(">")) { - buff.append(">"); - } else { - buff.append(str); - } - } - result = buff.toString(); - // now replace common text with - // the entities declared in the DTD - - Enumeration enu = entities.keys(); - while (enu.hasMoreElements()) { - String key = enu.nextElement(); - String value = entities.get(key); - if (!value.equals("") && !key.equals("amp") && !key.equals("lt") && !key.equals("gt") && !key.equals("quot") - && !key.equals("apos")) { - result = replaceEntities(result, value, "&" + key + ";"); - } - } - return result; - } - - private static void writeString(String string) throws IOException { - output.write(string.getBytes(encoding)); - } - - private static void loadSegments() throws SAXException, IOException, ParserConfigurationException { - - SAXBuilder builder = new SAXBuilder(); - if (catalog != null) { - builder.setEntityResolver(catalog); - } - - Document doc = builder.build(xliffFile); - Element root = doc.getRootElement(); - Element body = root.getChild("file").getChild("body"); - List units = body.getChildren("trans-unit"); - Iterator i = units.iterator(); - - segments = new Hashtable<>(); - - while (i.hasNext()) { - Element unit = i.next(); - if (dita_based) { - checkUntranslatable(unit); - } - segments.put(unit.getAttributeValue("id"), unit); - } - - entities = new Hashtable<>(); - - Element header = root.getChild("file").getChild("header"); - List groups = header.getChildren("prop-group"); - if (groups != null) { - Iterator g = groups.iterator(); - while (g.hasNext()) { - Element group = g.next(); - if (group.getAttributeValue("name").equals("entities")) { - List props = group.getChildren("prop"); - Iterator p = props.iterator(); - while (p.hasNext()) { - Element prop = p.next(); - entities.put(prop.getAttributeValue("prop-type"), prop.getText()); - } - } - if (group.getAttributeValue("name").equals("encoding")) { - String stored = group.getChild("prop").getText(); - if (!stored.equals(encoding)) { - encoding = stored; - } - } - } - } - } - - private static void checkUntranslatable(Element unit) { - Element source = unit.getChild("source"); - Element target = unit.getChild("target"); - if (target == null) { - return; - } - List slist = source.getChildren("mrk"); - List tlist = target.getChildren("mrk"); - for (int i = 0; i < slist.size(); i++) { - Element sg = slist.get(i); - if (!sg.getAttributeValue("mtype", "").equals("protected")) { - continue; - } - for (int j = 0; j < tlist.size(); j++) { - Element tg = tlist.get(j); - if (tg.getAttributeValue("mid", "").equals(sg.getAttributeValue("mid", "-"))) { - tg.setContent(sg.getContent()); - break; - } - } - } - } - - protected static String replaceToken(String string, String token, String newText) { - String result = string; - int index = result.indexOf(token); - while (index != -1) { - result = result.substring(0, index) + newText + result.substring(index + token.length()); - index = result.indexOf(token, index + newText.length()); - } - return result; - } -} +/******************************************************************************* + * Copyright (c) 2003-2019 Maxprograms. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/org/documents/epl-v10.html + * + * Contributors: + * Maxprograms - initial API and implementation + *******************************************************************************/ +package com.maxprograms.converters.xml; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.text.MessageFormat; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.StringTokenizer; +import java.util.Vector; +import java.lang.System.Logger.Level; +import java.lang.System.Logger; + +import javax.xml.parsers.ParserConfigurationException; + +import org.xml.sax.SAXException; + +import com.maxprograms.converters.Constants; +import com.maxprograms.xml.Catalog; +import com.maxprograms.xml.Document; +import com.maxprograms.xml.Element; +import com.maxprograms.xml.SAXBuilder; +import com.maxprograms.xml.TextNode; +import com.maxprograms.xml.XMLNode; +import com.maxprograms.xml.XMLOutputter; + +public class Xliff2Xml { + + private static final Logger LOGGER = System.getLogger(Xliff2Xml.class.getName()); + + private static InputStreamReader input; + private static BufferedReader buffer; + private static String sklFile; + private static String xliffFile; + private static String line; + private static Hashtable segments; + private static FileOutputStream output; + private static String encoding; + private static Catalog catalog; + private static Hashtable entities; + private static boolean inDesign = false; + private static boolean inAttribute; + private static boolean inCData; + private static boolean dita_based = false; + private static boolean IDML; + + private Xliff2Xml() { + // do not instantiate this class + // use run method instead + } + + public static Vector run(Hashtable params) { + + Vector result = new Vector<>(); + + sklFile = params.get("skeleton"); + xliffFile = params.get("xliff"); + encoding = params.get("encoding"); + String isInDesign = params.get("InDesign"); + if (isInDesign != null) { + inDesign = true; + } + String isIDML = params.get("IDML"); + if (isIDML != null) { + IDML = true; + } + String isDitaBased = params.get("dita_based"); + if (isDitaBased != null) { + dita_based = true; + } + try { + catalog = new Catalog(params.get("catalog")); + String outputFile = params.get("backfile"); + File f = new File(outputFile); + File p = f.getParentFile(); + if (p == null) { + p = new File(System.getProperty("user.dir")); + } + if (!p.exists()) { + p.mkdirs(); + } + if (!f.exists()) { + Files.createFile(Paths.get(f.toURI())); + } + output = new FileOutputStream(f); + loadSegments(); + input = new InputStreamReader(new FileInputStream(sklFile), StandardCharsets.UTF_8); + buffer = new BufferedReader(input); + line = buffer.readLine(); + while (line != null) { + line = line + "\n"; + if (line.indexOf("%%%") != -1) { + // + // contains translatable text + // + int index = line.indexOf("%%%"); + while (index != -1) { + String start = line.substring(0, index); + writeString(start); + line = line.substring(index + 3); + String code = line.substring(0, line.indexOf("%%%")); + line = line.substring(line.indexOf("%%%\n") + 4); + Element segment = segments.get(code); + if (segment != null) { + inAttribute = segment.getAttributeValue("restype", "").equals("x-attribute"); + inCData = segment.getAttributeValue("restype", "").equals("x-cdata"); + Element target = segment.getChild("target"); + Element source = segment.getChild("source"); + if (target != null) { + if (segment.getAttributeValue("approved", "no").equals("yes")) { + writeString(extractText(target)); + } else { + writeString(extractText(source)); + } + } else { + writeString(extractText(source)); + } + } else { + result.add(Constants.ERROR); + MessageFormat mf = new MessageFormat("Segment {0} not found."); + result.add(mf.format(new Object[] { code })); + return result; + } + + index = line.indexOf("%%%"); + if (index == -1) { + writeString(line); + } + } // end while + } else { + // + // non translatable portion + // + writeString(line); + } + line = buffer.readLine(); + } + + output.close(); + output = null; + if (dita_based) { + try { + removeTranslate(outputFile); + } catch (SAXException sax) { + LOGGER.log(Level.ERROR, "removeTranslate error: " + outputFile); + throw sax; + } + } + if (inDesign) { + removeSeparators(outputFile); + } + result.add(Constants.SUCCESS); + } catch (IOException | SAXException | ParserConfigurationException e) { + LOGGER.log(Level.ERROR, "Error merging file", e); + result.add(Constants.ERROR); + result.add(e.getMessage()); + } + return result; + } + + private static void removeTranslate(String outputFile) + throws SAXException, IOException, ParserConfigurationException { + SAXBuilder builder = new SAXBuilder(); + builder.setEntityResolver(catalog); + Document doc = builder.build(outputFile); + Element root = doc.getRootElement(); + removeTranslateAtt(root); + XMLOutputter outputter = new XMLOutputter(); + outputter.preserveSpace(true); + try (FileOutputStream out = new FileOutputStream(outputFile)) { + outputter.output(doc, out); + } + } + + private static void removeTranslateAtt(Element e) { + if (e.getAttributeValue("removeTranslate", "no").equals("yes")) { + e.removeAttribute("translate"); + e.removeAttribute("removeTranslate"); + } + List children = e.getChildren(); + for (Element child : children) { + removeTranslateAtt(child); + } + } + + private static void removeSeparators(String outputFile) + throws SAXException, IOException, ParserConfigurationException { + SAXBuilder builder = new SAXBuilder(); + builder.setEntityResolver(catalog); + Document doc = builder.build(outputFile); + Element root = doc.getRootElement(); + recurse(root); + XMLOutputter outputter = new XMLOutputter(); + outputter.preserveSpace(true); + try (FileOutputStream out = new FileOutputStream(outputFile)) { + outputter.output(doc, out); + } + } + + private static void recurse(Element e) { + List content = e.getContent(); + for (int i = 0; i < content.size(); i++) { + XMLNode n = content.get(i); + if (n.getNodeType() == XMLNode.TEXT_NODE) { + TextNode t = (TextNode) n; + if (t.getText().startsWith("c_")) { + t.setText("c_" + t.getText().substring(2).replaceAll("_", "~sep~")); + } + } + if (n.getNodeType() == XMLNode.ELEMENT_NODE) { + recurse((Element) n); + } + } + } + + private static String extractText(Element element) throws SAXException { + String result = ""; + List content = element.getContent(); + Iterator i = content.iterator(); + + if (element.getName().equals("ph")) { + return fixEntities(element); + } + if (dita_based && element.getName().equals("mrk")) { + return cleanMrk(element); + } + while (i.hasNext()) { + XMLNode n = i.next(); + switch (n.getNodeType()) { + case XMLNode.ELEMENT_NODE: + Element e = (Element) n; + String ph = extractText(e); + result = result + ph; + break; + case XMLNode.TEXT_NODE: + if (inAttribute) { + result = result + addEntities(((TextNode) n).getText()).replaceAll("\"", """); + } else if (inCData) { + result = result + ((TextNode) n).getText(); + } else { + String text = ((TextNode) n).getText(); + if (IDML && text.indexOf('\n') != -1) { + text = text.replaceAll("\\n", ""); + } + result = result + addEntities(text); + } + break; + default: + // ignore + break; + } + } + return result; + } + + private static String cleanMrk(Element element) throws SAXException { + String ts = element.getAttributeValue("ts", ""); + if (ts.isEmpty()) { + throw new SAXException("Broken element."); + } + ts = restoreChars(ts).trim(); + String name = ""; + for (int i = 1; i < ts.length(); i++) { + if (Character.isSpaceChar(ts.charAt(i))) { + break; + } + name = name + ts.charAt(i); + } + String content = ""; + List nodes = element.getContent(); + Iterator it = nodes.iterator(); + while (it.hasNext()) { + XMLNode n = it.next(); + switch (n.getNodeType()) { + case XMLNode.ELEMENT_NODE: + Element e = (Element) n; + String ph = extractText(e); + content = content + ph; + break; + case XMLNode.TEXT_NODE: + content = content + addEntities(((TextNode) n).getText()); + break; + default: + // ignore + break; + } + } + return ts + content + ""; // TODO recurse content + } + + private static String restoreChars(String string) { + String result = string.replaceAll(Xml2Xliff.MATHLT, "<"); + result = result.replaceAll(Xml2Xliff.MATHGT, ">"); + result = result.replaceAll(Xml2Xliff.DOUBLEPRIME, "\""); + result = result.replaceAll(Xml2Xliff.GAMP, "&"); + return result; + } + + public static String fixEntities(Element element) { + String string = element.getText(); + + int start = string.indexOf('&'); + String result = ""; + if (start > 0) { + result = string.substring(0, start); + string = string.substring(start); + } + while (start != -1) { + int colon = string.indexOf(';'); + if (colon == -1) { + // no ";", we are not in an entity + result = result + "&"; + string = string.substring(1); + } else { + boolean inEntity = true; + for (int i = 1; i < colon; i++) { + char c = string.charAt(i); + if (Character.isWhitespace(c) || "&.@$*()[]{},/?\\\"\'+=-^".indexOf(c) != -1) { + inEntity = false; + break; + } + } + if (!inEntity) { + result = result + "&"; + string = string.substring(1); + } else { + result = result + string.substring(0, colon + 1); + string = string.substring(colon + 1); + } + } + start = string.indexOf('&'); + if (start > 0) { + result = result + string.substring(0, start); + string = string.substring(start); + } + } + + return (result + string).replaceAll("###AMP###", "&"); + } + + private static String replaceEntities(String original, String token, String entity) { + String result = original; + int index = result.indexOf(token); + while (index != -1) { + String before = result.substring(0, index); + String after = result.substring(index + token.length()); + // check if we are not inside an entity + int amp = before.lastIndexOf('&'); + if (amp == -1) { + // we are not in an entity + result = before + entity + after; + } else { + boolean inEntity = true; + for (int i = amp; i < before.length(); i++) { + char c = before.charAt(i); + if (Character.isWhitespace(c) || ";.@$*()[]{},/?\\\"\'+=-^".indexOf(c) != -1) { + inEntity = false; + break; + } + } + if (inEntity) { + // check for a colon in "after" + int colon = after.indexOf(';'); + if (colon == -1) { + // we are not in an entity + result = before + entity + after; + } else { + // verify is there is something that breaks the entity before + for (int i = 0; i < colon; i++) { + char c = after.charAt(i); + if (Character.isWhitespace(c) || "&.@$*()[]{},/?\\\"\'+=-^".indexOf(c) != -1) { + break; + } + } + } + } else { + // we are not in an entity + result = before + entity + after; + } + } + if (index < result.length()) { + index = result.indexOf(token, index + 1); + } + } + return result; + } + + private static String addEntities(String string) { + String result = string; + int index = result.indexOf('&'); + while (index != -1) { + int smcolon = result.indexOf(';', index); + if (smcolon == -1) { + // no semicolon. there is no chance it is an entity + result = result.substring(0, index) + "&" + result.substring(index + 1); + index++; + } else { + int space = result.indexOf(' ', index); + if (space == -1) { + String name = result.substring(index + 1, smcolon); + if (entities.containsKey(name)) { + // it is an entity, jump to the semicolon + index = smcolon; + } else { + result = result.substring(0, index) + "&" + result.substring(index + 1); + index++; + } + } else { + // check if space appears before the semicolon + if (space < smcolon) { + // not an entity! + result = result.substring(0, index) + "&" + result.substring(index + 1); + index++; + } else { + String name = result.substring(index + 1, smcolon); + if (entities.containsKey(name)) { + // it is a known entity, jump to the semicolon + index = smcolon; + } else { + result = result.substring(0, index) + "&" + result.substring(index + 1); + index++; + } + } + } + } + if (index < result.length() && index >= 0) { + index = result.indexOf('&', index); + } else { + index = -1; + } + } + StringTokenizer tok = new StringTokenizer(result, "<>", true); + StringBuilder buff = new StringBuilder(); + while (tok.hasMoreElements()) { + String str = tok.nextToken(); + if (str.equals("<")) { + buff.append("<"); + } else if (str.equals(">")) { + buff.append(">"); + } else { + buff.append(str); + } + } + result = buff.toString(); + // now replace common text with + // the entities declared in the DTD + + Enumeration enu = entities.keys(); + while (enu.hasMoreElements()) { + String key = enu.nextElement(); + String value = entities.get(key); + if (!value.equals("") && !key.equals("amp") && !key.equals("lt") && !key.equals("gt") && !key.equals("quot") + && !key.equals("apos")) { + result = replaceEntities(result, value, "&" + key + ";"); + } + } + return result; + } + + private static void writeString(String string) throws IOException { + output.write(string.getBytes(encoding)); + } + + private static void loadSegments() throws SAXException, IOException, ParserConfigurationException { + + SAXBuilder builder = new SAXBuilder(); + if (catalog != null) { + builder.setEntityResolver(catalog); + } + + Document doc = builder.build(xliffFile); + Element root = doc.getRootElement(); + Element body = root.getChild("file").getChild("body"); + List units = body.getChildren("trans-unit"); + Iterator i = units.iterator(); + + segments = new Hashtable<>(); + + while (i.hasNext()) { + Element unit = i.next(); + if (dita_based) { + checkUntranslatable(unit); + } + segments.put(unit.getAttributeValue("id"), unit); + } + + entities = new Hashtable<>(); + + Element header = root.getChild("file").getChild("header"); + List groups = header.getChildren("prop-group"); + if (groups != null) { + Iterator g = groups.iterator(); + while (g.hasNext()) { + Element group = g.next(); + if (group.getAttributeValue("name").equals("entities")) { + List props = group.getChildren("prop"); + Iterator p = props.iterator(); + while (p.hasNext()) { + Element prop = p.next(); + entities.put(prop.getAttributeValue("prop-type"), prop.getText()); + } + } + if (group.getAttributeValue("name").equals("encoding")) { + String stored = group.getChild("prop").getText(); + if (!stored.equals(encoding)) { + encoding = stored; + } + } + } + } + } + + private static void checkUntranslatable(Element unit) { + Element source = unit.getChild("source"); + Element target = unit.getChild("target"); + if (target == null) { + return; + } + List slist = source.getChildren("mrk"); + List tlist = target.getChildren("mrk"); + for (int i = 0; i < slist.size(); i++) { + Element sg = slist.get(i); + if (!sg.getAttributeValue("mtype", "").equals("protected")) { + continue; + } + for (int j = 0; j < tlist.size(); j++) { + Element tg = tlist.get(j); + if (tg.getAttributeValue("mid", "").equals(sg.getAttributeValue("mid", "-"))) { + tg.setContent(sg.getContent()); + break; + } + } + } + } + + protected static String replaceToken(String string, String token, String newText) { + String result = string; + int index = result.indexOf(token); + while (index != -1) { + result = result.substring(0, index) + newText + result.substring(index + token.length()); + index = result.indexOf(token, index + newText.length()); + } + return result; + } +} diff --git a/src/com/maxprograms/converters/xml/Xml2Xliff.java b/src/com/maxprograms/converters/xml/Xml2Xliff.java index 2e795e60..855f58f9 100644 --- a/src/com/maxprograms/converters/xml/Xml2Xliff.java +++ b/src/com/maxprograms/converters/xml/Xml2Xliff.java @@ -1,1558 +1,1559 @@ -/******************************************************************************* - * Copyright (c) 2003-2019 Maxprograms. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 1.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/org/documents/epl-v10.html - * - * Contributors: - * Maxprograms - initial API and implementation - *******************************************************************************/ -package com.maxprograms.converters.xml; - -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.FileReader; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.List; -import java.util.Stack; -import java.util.StringTokenizer; -import java.util.Vector; -import java.lang.System.Logger.Level; -import java.lang.System.Logger; - -import javax.xml.parsers.ParserConfigurationException; - -import org.xml.sax.SAXException; - -import com.maxprograms.converters.Utils; -import com.maxprograms.segmenter.Segmenter; -import com.maxprograms.xml.Attribute; -import com.maxprograms.xml.CData; -import com.maxprograms.xml.Catalog; -import com.maxprograms.xml.Comment; -import com.maxprograms.xml.Document; -import com.maxprograms.xml.Element; -import com.maxprograms.xml.SAXBuilder; -import com.maxprograms.xml.SilentErrorHandler; -import com.maxprograms.xml.TextNode; -import com.maxprograms.xml.XMLNode; -import com.maxprograms.xml.XMLUtils; -import com.wutka.dtd.DTD; -import com.wutka.dtd.DTDParser; - -public class Xml2Xliff { - - private static final Logger LOGGER = System.getLogger(Xml2Xliff.class.getName()); - - private static final String STARTG = "%%START%%"; - private static final String ENDG = "%%END%%"; - - static final String DOUBLEPRIME = "\u2033"; - static final String MATHLT = "\u2039"; - static final String MATHGT = "\u200B\u203A"; - static final String GAMP = "\u200B\u203A"; - - private static String inputFile; - private static String skeletonFile; - private static String sourceLanguage; - private static String srcEncoding; - private static FileOutputStream output; - private static FileOutputStream skeleton; - private static int segId; - private static int tagId; - private static List segments; - private static Hashtable startsSegment; - private static Hashtable> translatableAttributes; - private static Hashtable inline; - private static Hashtable ctypes; - private static Hashtable keepFormating; - private static boolean segByElement; - private static Segmenter segmenter; - private static String catalog; - private static String rootElement; - private static Hashtable entities; - private static String entitiesMap; - private static Element root; - private static String text; - private static Stack stack; - private static String translatable = ""; - private static boolean inDesign = false; - private static Hashtable ignore; - private static boolean generic; - private static boolean resx; - private static String startText; - private static String endText; - private static boolean dita_based; - private static String targetLanguage; - private static boolean inCData; - private static boolean translateComments; - - private Xml2Xliff() { - // do not instantiate this class - // use run method instead - } - - public static Vector run(Hashtable params) { - Vector result = new Vector<>(); - segId = 1; - stack = new Stack<>(); - - inputFile = params.get("source"); - String xliffFile = params.get("xliff"); - skeletonFile = params.get("skeleton"); - sourceLanguage = params.get("srcLang"); - targetLanguage = params.get("tgtLang"); - srcEncoding = params.get("srcEncoding"); - catalog = params.get("catalog"); - String elementSegmentation = params.get("paragraph"); - String initSegmenter = params.get("srxFile"); - String isInDesign = params.get("InDesign"); - if (isInDesign != null) { - inDesign = true; - } else { - inDesign = false; - } - String isResx = params.get("resx"); - if (isResx != null) { - resx = true; - } else { - resx = false; - } - String dita = params.get("dita_based"); - if (dita != null) { - dita_based = dita.equalsIgnoreCase("yes"); - } - - String isGeneric = params.get("generic"); - if (isGeneric != null && isGeneric.equals("yes")) { - generic = true; - } else { - generic = false; - } - - String comments = params.get("translateComments"); - if (comments != null) { - translateComments = comments.equalsIgnoreCase("yes"); - } - - try { - boolean autoConfiguration = false; - String iniFile = getIniFile(inputFile); - File f = new File(iniFile); - if (!f.exists()) { - if (!generic) { - MessageFormat mf = new MessageFormat( - "Configuration file ''{0}'' not found. \\n\\nWrite a new configuration file for the XML Converter or set file type to ''XML (Generic)''."); - throw new IOException(mf.format(new Object[] { f.getName() })); - } - AutoConfiguration.run(inputFile, f.getAbsolutePath(), catalog); - autoConfiguration = true; - } - - if (elementSegmentation == null) { - segByElement = false; - } else { - if (elementSegmentation.equals("yes")) { - segByElement = true; - } else { - segByElement = false; - } - } - - if (segByElement == false) { - segmenter = new Segmenter(initSegmenter, sourceLanguage, catalog); - } - - String detected = getEncoding(inputFile); - if (!srcEncoding.equals(detected)) { - srcEncoding = detected; - } - - try (FileInputStream input = new FileInputStream(inputFile)) { - skeleton = new FileOutputStream(skeletonFile); - output = new FileOutputStream(xliffFile); - writeHeader(); - - int size = input.available(); - byte[] array = new byte[size]; - if (size != input.read(array)) { - result.add("1"); - result.add("Error reading from input file."); - return result; - } - String file = new String(array, srcEncoding); - // remove xml declaration and doctype - int begin = file.indexOf("<" + rootElement); - if (begin != -1) { - if (file.charAt(0) == '<') { - writeSkeleton(file.substring(0, begin)); - } else { - writeSkeleton(file.substring(1, begin)); - } - } - - buildTables(iniFile); - - if (autoConfiguration) { - Files.delete(Paths.get(f.toURI())); - } - - buildList(); - - processList(); - - skeleton.close(); - writeString("\n"); - writeString("\n"); - writeString(""); - } - output.close(); - - result.add("0"); - - } catch (IOException | SAXException | ParserConfigurationException e) { - LOGGER.log(Level.ERROR, "Error converting XML file", e); - result.add("1"); - result.add(e.getMessage()); - } - - return result; - } - - public static String getIniFile(String fileName) throws SAXException, IOException, ParserConfigurationException { - File folder = new File(System.getProperty("user.dir"), "xmlfilter"); - SAXBuilder builder = new SAXBuilder(); - Catalog cat = new Catalog(catalog); - builder.setEntityResolver(cat); - builder.setValidating(false); - builder.setErrorHandler(new SilentErrorHandler()); - /* - * set expandEntities to false if automatic inclusion of referenced documents is - * not desired. It is enabled by default, but may change later upon request. - * - */ - // builder.expandEntities(false); - Document doc = builder.build(fileName); - entities = new Hashtable<>(); - - Hashtable map = doc.getEntities(); - - entitiesMap = ""; - if (map != null) { - Enumeration en = entities.keys(); - while (en.hasMoreElements()) { - String key = en.nextElement(); - entitiesMap = entitiesMap + " " - + cleanEntity(entities.get(key)) + "\n"; - } - } - - // Add predefined standard entities - entities.put(">", ">"); - entities.put("<", "<"); - entities.put("&", "&"); - entities.put("'", "'"); - entities.put(""", "\""); - - root = doc.getRootElement(); - rootElement = root.getName(); - if (dita_based && rootElement.equals("svg")) { - dita_based = false; - } - - // check for ResX before anything else - // this requires a fixed ini name - if (root.getName().equals("root")) { - List dataElements = root.getChildren("data"); - if (dataElements.size() > 0) { - boolean isResx = false; - for (int i = 0; i < dataElements.size(); i++) { - Element g = (dataElements.get(i)).getChild("translate"); - if (g != null) { - isResx = true; - break; - } - } - if (isResx) { - return new File(folder, "config_resx.xml").getAbsolutePath(); - } - } - } - String pub = doc.getPublicId(); - String sys = doc.getSystemId(); - if (sys != null) { - // remove path from systemId - if (sys.indexOf('/') != -1 && sys.lastIndexOf('/') < sys.length()) { - sys = sys.substring(sys.lastIndexOf('/') + 1); - } - if (sys.indexOf('\\') != -1 && sys.lastIndexOf('/') < sys.length()) { - sys = sys.substring(sys.lastIndexOf('\\') + 1); - } - } - - Document d = builder.build(catalog); - Element r = d.getRootElement(); - List dtds = r.getChildren("dtd"); - Iterator i = dtds.iterator(); - while (i.hasNext()) { - Element dtd = i.next(); - if (pub != null && dtd.getAttributeValue("publicId", "").equals(pub)) { - String s = getRootElement(dtd.getText()); - if (s != null) { - return new File(folder, "config_" + s + ".xml").getAbsolutePath(); - } - } - if (sys != null && dtd.getAttributeValue("systemId", "").equals(sys)) { - String s = getRootElement(dtd.getText()); - if (s != null) { - return new File(folder, "config_" + s + ".xml").getAbsolutePath(); - } - if (dtd.getAttributeValue("systemId", "").endsWith(sys)) { - String st = getRootElement(dtd.getText()); - if (st != null) { - return new File(folder, "config_" + st + ".xml").getAbsolutePath(); - } - } - } - } - - if (rootElement.indexOf(':') != -1) { - return new File(folder, "config_" + rootElement.substring(0, rootElement.indexOf(':')) + ".xml") - .getAbsolutePath(); - } - - File f = new File(folder, "config_" + rootElement + ".xml"); - if (!f.exists() && dita_based) { - File base = new File(folder, "config_dita.xml"); - Document dd = builder.build(base); - List list = dd.getRootElement().getChildren(); - Iterator it = list.iterator(); - while (it.hasNext()) { - if (rootElement.equals(it.next().getText().trim())) { - return base.getAbsolutePath(); - } - } - String cls = root.getAttributeValue("class", ""); - String[] parts = cls.split("\\s"); - for (int h = 0; h < parts.length; h++) { - String part = parts[h]; - if (part.indexOf('/') == -1) { - continue; - } - String code = part.substring(part.indexOf('/') + 1).trim(); - it = list.iterator(); - while (it.hasNext()) { - if (code.equals(it.next().getText().trim())) { - return base.getAbsolutePath(); - } - } - } - } - return new File(folder, "config_" + rootElement + ".xml").getAbsolutePath(); - } - - private static String cleanEntity(String string) { - String result = string; - int control = result.indexOf('&'); - while (control != -1) { - int sc = result.indexOf(';', control); - if (sc == -1) { - // no semicolon, it's not an entity - result = result.substring(0, control) + "&" + result.substring(control + 1); - } else { - // may be an entity - String candidate = result.substring(control, sc) + ";"; - if (!candidate.equals("&")) { - String entity = entities.get(candidate); - if (entity != null) { - result = result.substring(0, control) + entity + result.substring(sc + 1); - } else if (candidate.startsWith("&#x")) { - // it's a character in hexadecimal format - int c = Integer.parseInt(candidate.substring(3, candidate.length() - 1), 16); - result = result.substring(0, control) + (char) c + result.substring(sc + 1); - } else if (candidate.startsWith("&#")) { - // it's a character - int c = Integer.parseInt(candidate.substring(2, candidate.length() - 1)); - result = result.substring(0, control) + (char) c + result.substring(sc + 1); - } else { - result = result.substring(0, control) + "&" + result.substring(control + 1); - } - } - } - if (control < result.length()) { - control++; - } - control = result.indexOf('&', control); - } - - control = result.indexOf('<'); - while (control != -1) { - result = result.substring(0, control) + "<" + result.substring(control + 1); - if (control < result.length()) { - control++; - } - control = result.indexOf('<', control); - } - - control = result.indexOf('>'); - while (control != -1) { - result = result.substring(0, control) + ">" + result.substring(control + 1); - if (control < result.length()) { - control++; - } - control = result.indexOf('>', control); - } - return result; - } - - private static String getRootElement(String file) { - String result = null; - File dtd = new File(file); - try { - DTDParser parser = new DTDParser(dtd); - DTD d = parser.parse(true); - if (d != null && d.rootElement != null) { - result = d.rootElement.getName(); - } - } catch (IOException e) { - LOGGER.log(Level.WARNING, "Error getting root element from DTD", e); - } - return result; - } - - private static void writeHeader() throws IOException { - String tgtLang = ""; - if (targetLanguage != null) { - tgtLang = "\" target-language=\"" + targetLanguage; - } - - String format = "xml"; - if (inDesign) { - format = "x-inx"; - } else if (resx) { - format = "resx"; - } - writeString("\n"); - writeString("\n"); - writeString("\n"); - - writeString("\n"); - writeString("
\n"); - writeString(" \n"); - writeString(" \n"); - writeString(" \n"); - if (!entitiesMap.equals("")) { - writeString(" \n" + entitiesMap + " \n"); - } - writeString("
\n"); - writeString("\n"); - } - - private static void processList() throws IOException, SAXException, ParserConfigurationException { - for (int i = 0; i < segments.size(); i++) { - String txt = segments.get(i); - - if (txt.startsWith("" + '\u007F' + "" + '\u007F')) { - // send directly to skeleton - writeSkeleton(txt.substring(2)); - continue; - } - if (txt.startsWith("" + '\u0081')) { - inCData = true; - txt = txt.substring(1); - } else { - inCData = false; - } - if (inDesign && !txt.trim().equals("")) { - if (txt.startsWith("c_") && !txt.substring(2).trim().equals("")) { - writeSkeleton("c_"); - txt = txt.substring(2); - txt = txt.replaceAll("~sep~", "_"); - } else { - writeSkeleton(txt); - continue; - } - } - tagId = 0; - if (dita_based) { - txt = prepareG(txt); - } - txt = addTags(txt); - if (segByElement == true) { - writeSegment(txt); - } else { - String[] segs = segmenter.segment(txt); - for (int h = 0; h < segs.length; h++) { - String seg = segs[h]; - while (seg.startsWith("" + '\u2029')) { - writeSkeleton("" + '\u2029'); - seg = seg.substring(1); - } - writeSegment(seg); - } - } - } - } - - private static String prepareG(String string) { - int start = string.indexOf(STARTG); - if (start == -1) { - return string; - } - String txt = string; - String result = txt.substring(0, start); - while (start != -1) { - txt = txt.substring(start + STARTG.length()); - start = txt.indexOf(STARTG); - String element = txt.substring(0, start); - result = result + makeMrk(element); - txt = txt.substring(start + STARTG.length()); - int end = txt.indexOf(ENDG); - String content = txt.substring(0, end); - result = result + content + "
"; - end = txt.indexOf(ENDG, end + 1); - txt = txt.substring(end + ENDG.length()); - start = txt.indexOf(STARTG); - if (start != -1) { - result = result + txt.substring(0, start); - } - } - return result + txt; - } - - private static void writeSegment(String tagged) throws IOException, SAXException, ParserConfigurationException { - String restype = ""; - if (!containsText(tagged)) { - String untagged = removeTags(tagged); - writeSkeleton(untagged); - return; - } - if (inCData) { - restype = " restype=\"x-cdata\""; - } - String seg = " \n" - + " " + tagged + "\n \n"; - - String clean = tidy(seg); - String dirt = startText + "%%%" + segId++ + "%%%\n" + endText; - writeString(clean); - writeSkeleton(dirt); - } - - private static String removeTags(String tagged) throws IOException, SAXException, ParserConfigurationException { - String source = "" + tagged + ""; - SAXBuilder b = new SAXBuilder(); - Document d = null; - try { - d = b.build(new ByteArrayInputStream(source.getBytes(StandardCharsets.UTF_8))); - } catch (SAXException sax) { - LOGGER.log(Level.ERROR, "Broken segment: " + source); - throw sax; - } - Element r = d.getRootElement(); - return extractText(r); - } - - private static String extractText(Element element) throws SAXException { - if (element.getName().equals("ph")) { - return Xliff2Xml.fixEntities(element); - } - if (dita_based && element.getName().equals("g")) { - return cleanMrk(element); - } - - String result = ""; - List content = element.getContent(); - Iterator i = content.iterator(); - while (i.hasNext()) { - XMLNode n = i.next(); - switch (n.getNodeType()) { - case XMLNode.ELEMENT_NODE: - Element e = (Element) n; - if (e.getName().equals("ph")) { - String ph = extractText(e); - result = result + ph; - } else if (e.getName().equals("mrk")) { - String mrk = cleanMrk(e); - result = result + mrk; - } else { - throw new SAXException("broken tagged text"); - } - break; - case XMLNode.TEXT_NODE: - if (inCData) { - result = result + ((TextNode) n).getText(); - } else { - result = result + addEntities(((TextNode) n).getText()); - } - break; - default: - // ignore - break; - } - } - return result; - } - - private static String cleanMrk(Element element) throws SAXException { - String ts = element.getAttributeValue("ts", ""); - if (ts.isEmpty()) { - throw new SAXException("Broken element."); - } - ts = restoreChars(ts).trim(); - String name = ""; - for (int i = 1; i < ts.length(); i++) { - if (Character.isSpaceChar(ts.charAt(i))) { - break; - } - name = name + ts.charAt(i); - } - String content = ""; - List nodes = element.getContent(); - Iterator it = nodes.iterator(); - while (it.hasNext()) { - XMLNode n = it.next(); - switch (n.getNodeType()) { - case XMLNode.ELEMENT_NODE: - Element e = (Element) n; - String ph = extractText(e); - content = content + ph; - break; - case XMLNode.TEXT_NODE: - content = content + XMLUtils.cleanText(((TextNode) n).getText()); - break; - default: - // ignore - break; - } - } - return ts + content + ""; // TODO recurse content - } - - private static String restoreChars(String string) { - String result = string.replaceAll(Xml2Xliff.MATHLT, "<"); - result = result.replaceAll(Xml2Xliff.MATHGT, ">"); - result = result.replaceAll(Xml2Xliff.DOUBLEPRIME, "\""); - result = result.replaceAll(Xml2Xliff.GAMP, "&"); - return result; - } - - private static String addEntities(String string) { - String result = string.replaceAll("<", "<"); - result = result.replaceAll(">", ">"); - result = result.replaceAll(""", "\""); - result = result.replaceAll("&", "&"); - return result; - } - - private static String tidy(String seg) throws SAXException, IOException, ParserConfigurationException { - startText = ""; - endText = ""; - SAXBuilder b = new SAXBuilder(); - Document d = b.build(new ByteArrayInputStream(seg.getBytes(StandardCharsets.UTF_8))); - Element r = d.getRootElement(); - Element s = r.getChild("source"); - if (s.getChildren().size() == 0) { - return seg; - } - List start = new ArrayList<>(); - List end = new ArrayList<>(); - List txt = new ArrayList<>(); - - List content = s.getContent(); - - Vector startTags = new Vector<>(); - Vector endTags = new Vector<>(); - - for (int i = 0; i < content.size(); i++) { - XMLNode n = content.get(i); - if (n.getNodeType() == XMLNode.TEXT_NODE && !n.toString().trim().equals("")) { - break; - } - if (n.getNodeType() == XMLNode.ELEMENT_NODE) { - Element e = (Element) n; - if (!e.getName().equals("ph")) { - break; - } - startTags.add(e); - } - start.add(n); - } - - if (startTags.size() == 0) { - start.clear(); - } - for (int i = content.size() - 1; i >= 0; i--) { - XMLNode n = content.get(i); - if (n.getNodeType() == XMLNode.TEXT_NODE && !n.toString().trim().equals("")) { - break; - } - if (n.getNodeType() == XMLNode.ELEMENT_NODE) { - Element e = (Element) n; - if (!e.getName().equals("ph")) { - break; - } - endTags.add(0, e); - } - end.add(0, n); - } - if (endTags.size() == 0) { - end.clear(); - } - - int trimmed = 0; - - if (startTags.size() > 0 && endTags.size() > 0) { - for (int i = 0; i < startTags.size() && i < endTags.size(); i++) { - Element f = (Element) startTags.get(i); - Element l = (Element) endTags.get(endTags.size() - 1 - i); - if ((l.getText().startsWith("")) - && (!(f.getText().startsWith("")))) { - String endTag = l.getText().substring(2); - endTag = endTag.substring(0, endTag.length() - 1); - if (f.getText().startsWith("<" + endTag)) { - // matched - trimmed++; - } - } - } - } - - if (trimmed > 0) { - List start2 = new ArrayList<>(); - List end2 = new ArrayList<>(); - - int count = 0; - for (int h = 0; h < start.size(); h++) { - XMLNode n = start.get(h); - start2.add(n); - if (n.getNodeType() == XMLNode.ELEMENT_NODE) { - count++; - if (count == trimmed) { - break; - } - } - } - start = start2; - count = 0; - for (int h = end.size() - 1; h >= 0; h--) { - XMLNode n = end.get(h); - end2.add(0, n); - if (n.getNodeType() == XMLNode.ELEMENT_NODE) { - count++; - if (count == trimmed) { - break; - } - } - } - end = end2; - } else { - if (startTags.size() == 1 && s.getChildren().size() == 1) { - // send initial tag to skeleton, keep end spaces - end.clear(); - } else if (s.getChildren().size() == 1 && endTags.size() == 1) { - // set ending tag to skeleton, keep initial spaces - start.clear(); - } else { - start.clear(); - end.clear(); - } - } - - for (int i = start.size(); i < content.size() - end.size(); i++) { - txt.add(content.get(i)); - } - - for (int i = 0; i < start.size(); i++) { - XMLNode n = start.get(i); - if (n.getNodeType() == XMLNode.TEXT_NODE) { - startText += ((TextNode) n).getText(); - } - if (n.getNodeType() == XMLNode.ELEMENT_NODE) { - startText += ((Element) n).getText(); - } - } - for (int i = 0; i < end.size(); i++) { - XMLNode n = end.get(i); - if (n.getNodeType() == XMLNode.TEXT_NODE) { - endText += ((TextNode) n).getText(); - } - if (n.getNodeType() == XMLNode.ELEMENT_NODE) { - endText += ((Element) n).getText(); - } - } - s.setContent(txt); - List children = s.getChildren("ph"); - for (int id = 0; id < children.size(); id++) { - Element child = children.get(id); - child.setAttribute("id", "" + id); - } - - return r.toString(); - } - - private static boolean containsText(String string) { - String tagged = string; - int start = tagged.indexOf(""); - if (dita_based) { - while (start != -1 && end != -1) { - tagged = tagged.substring(0, start) + tagged.substring(end + 6); - start = tagged.indexOf("", start + 5); - } else { - end = -1; - } - } - } - start = tagged.indexOf(""); - - while (start != -1 && end != -1) { - tagged = tagged.substring(0, start) + tagged.substring(end + 5); - start = tagged.indexOf("", start + 4); - } else { - end = -1; - } - } - - tagged = tagged.trim(); - if (tagged.length() == 0) { - return false; - } - for (int i = 0; i < tagged.length(); i++) { - int c = tagged.charAt(i); - if (" \u00A0\r\n\f\t\u2028\u2029,.;\":<>¿?¡!()[]{}=+/*\u00AB\u00BB\u201C\u201D\u201E\uFF00" - .indexOf(c) == -1) { - return true; - } - } - return false; - } - - private static String normalize(String string) { - String result = string.replace('\n', ' '); - result = result.replace('\t', ' '); - result = result.replace('\r', ' '); - result = result.replace('\f', ' '); - String rs = ""; - int length = result.length(); - for (int i = 0; i < length; i++) { - char ch = result.charAt(i); - if (ch != ' ') { - rs = rs + ch; - } else { - rs = rs + ch; - while (i < (length - 1) && result.charAt(i + 1) == ' ') { - i++; - } - } - } - return rs; - } - - private static String addTags(String string) { - String src = string; - String result = ""; - int start = src.indexOf('<'); - int end = src.indexOf('>'); - - while (start != -1) { - if (start > 0) { - result = result + cleanString(src.substring(0, start)); - src = src.substring(start); - start = src.indexOf('<'); - end = src.indexOf('>'); - } - String element = src.substring(start, end + 1); - src = src.substring(end + 1); - if (dita_based) { - if (!(element.startsWith(""))) { - result = result + tag(element); - } else { - result = result + element; - } - } else { - result = result + tag(element); - } - start = src.indexOf('<'); - end = src.indexOf('>'); - } - result = result + cleanString(src); - return result; - } - - private static String makeMrk(String element) { - return ""; - } - - private static String clean(String string) { - String result = string.replaceAll("<", MATHLT); - result = result.replaceAll(">", MATHGT); - result = result.replaceAll("\"", DOUBLEPRIME); - return replaceAmp(result); - } - - private static String replaceAmp(String value) { - StringBuilder result = new StringBuilder(); - for (int i = 0; i < value.length(); i++) { - char c = value.charAt(i); - if (c == '&') { - result.append(GAMP); - } else { - result.append(c); - } - } - return result.toString(); - } - - private static String tag(String element) { - String result = ""; - String type = getType(element); - if (translatableAttributes.containsKey(type)) { - result = extractAttributes(type, element); - } else { - String ctype = ""; - if (ctypes.containsKey(type)) { - ctype = " ctype=\"" + ctypes.get(type) + "\""; - } - result = "" + cleanString(element) + ""; - } - return result; - } - - private static String cleanString(String string) { - String s = string; - int control = s.indexOf('&'); - while (control != -1) { - int sc = s.indexOf(";", control); - if (sc == -1) { - // no semicolon, it's not an entity - s = s.substring(0, control) + "&" + s.substring(control + 1); - } else { - // may be an entity - String candidate = s.substring(control, sc) + ";"; - if (validEntitiy(candidate)) { - if (!candidate.equals("&") && !candidate.equals(""")) { - String entity = entities.get(candidate); - if (entity != null) { - s = s.substring(0, control) + entity + s.substring(sc + 1); - } else { - s = s.substring(0, control) + "%%%ph id=\"" + tagId++ + "\"%%%&" - + candidate.substring(1) + "%%%/ph%%%" + s.substring(sc + 1); - } - } else { - // it is an "&" - s = s.substring(0, control) + "&" + s.substring(control + 1); - } - } else { - // treat as an "&" - s = s.substring(0, control) + "&" + s.substring(control + 1); - } - } - if (control < s.length()) { - control++; - } - control = s.indexOf('&', control); - } - - control = s.indexOf('<'); - while (control != -1) { - s = s.substring(0, control) + "<" + s.substring(control + 1); - if (control < s.length()) { - control++; - } - control = s.indexOf('<', control); - } - - control = s.indexOf('>'); - while (control != -1) { - s = s.substring(0, control) + ">" + s.substring(control + 1); - if (control < s.length()) { - control++; - } - control = s.indexOf('>', control); - } - s = s.replaceAll("%%%/ph%%%", "
"); - s = s.replaceAll("%%%ph", "&"); - return XMLUtils.validChars(s); - } - - private static boolean validEntitiy(String candidate) { - if (candidate.length() < 3) { - return false; - } - char nameStart = candidate.charAt(1); - // ":" | [A-Z] | "_" | [a-z] | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF] | - // [#x370-#x37D] | [#x37F-#x1FFF] | [#x200C-#x200D] | [#x2070-#x218F] | - // [#x2C00-#x2FEF] | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] | - // [#x10000-#xEFFFF] - if (nameStart == ':' || (nameStart >= 'A' && nameStart <= 'Z') || (nameStart >= 'a' && nameStart <= 'z') - || (nameStart >= '\u00C0' && nameStart <= '\u00D6') || (nameStart >= '\u00D8' && nameStart <= '\u00F6') - || (nameStart >= '\u00F8' && nameStart <= '\u02FF') || (nameStart >= '\u0370' && nameStart <= '\u037D') - || (nameStart >= '\u037F' && nameStart <= '\u1FFF') || (nameStart >= '\u200C' && nameStart <= '\u200D') - || (nameStart >= '\u2017' && nameStart <= '\u218F') || (nameStart >= '\u2C00' && nameStart <= '\u2FEF') - || (nameStart >= '\u3001' && nameStart <= '\uD7FF') || (nameStart >= '\uF900' && nameStart <= '\uFDCF') - || (nameStart >= '\uFDF0' && nameStart <= '\uFFFD')) // not considered [#x10000-#xEFFFF] - { - // its OK - } else { - return false; - } - for (int i = 2; i < candidate.length() - 1; i++) { - // valid = nameStart | "-" | "." | [0-9] | #xB7 | [#x0300-#x036F] | - // [#x203F-#x2040] - char nameChar = candidate.charAt(i); - if (nameChar == ':' || (nameChar >= 'A' && nameChar <= 'Z') || (nameChar >= 'a' && nameChar <= 'z') - || (nameChar >= '\u00C0' && nameChar <= '\u00D6') || (nameChar >= '\u00D8' && nameChar <= '\u00F6') - || (nameChar >= '\u00F8' && nameChar <= '\u02FF') || (nameChar >= '\u0370' && nameChar <= '\u037D') - || (nameChar >= '\u037F' && nameChar <= '\u1FFF') || (nameChar >= '\u200C' && nameChar <= '\u200D') - || (nameChar >= '\u2017' && nameChar <= '\u218F') || (nameChar >= '\u2C00' && nameChar <= '\u2FEF') - || (nameChar >= '\u3001' && nameChar <= '\uD7FF') || (nameChar >= '\uF900' && nameChar <= '\uFDCF') - || (nameChar >= '\uFDF0' && nameChar <= '\uFFFD') || nameChar == '-' - || (nameChar >= '0' && nameChar <= '9') || (nameChar >= '\u0300' && nameChar <= '\u036F') - || (nameChar >= '\u203F' && nameChar <= '\u2040')) { - // its OK - } else { - return false; - } - } - return true; - } - - private static void writeSkeleton(String string) throws IOException { - skeleton.write(string.getBytes(StandardCharsets.UTF_8)); - } - - private static void writeString(String string) throws IOException { - output.write(string.getBytes(StandardCharsets.UTF_8)); - } - - private static String extractAttributes(String type, String element) { - - String ctype = ""; - if (ctypes.containsKey(type)) { - ctype = " ctype=\"" + ctypes.get(type) + "\""; - } - String result = ""; - String clean = cleanString(element); - - Vector v = translatableAttributes.get(type); - - StringTokenizer tokenizer = new StringTokenizer(clean, "&= \t\n\r\f/", true); - - while (tokenizer.hasMoreTokens()) { - String token = tokenizer.nextToken(); - if (!v.contains(token)) { - result = result + token; - } else { - result = result + token; - String s = tokenizer.nextToken(); - while (s.equals("=") || s.equals(" ")) { - result = result + s; - s = tokenizer.nextToken(); - } - // s contains the first word of the attribute - if (((s.startsWith("\"") && s.endsWith("\"")) || (s.startsWith("'") && s.endsWith("'"))) - && s.length() > 1) { - // the value is one word and it is quoted - result = result + s.substring(0, 1) + "" + s.substring(1, s.length() - 1) + "" + s.substring(s.length() - 1); - } else { - if (s.startsWith("\"") || s.startsWith("'")) { - // attribute value is quoted, but it has more than one - // word - String quote = s.substring(0, 1); - result = result + s.substring(0, 1) + "" + s.substring(1); - s = tokenizer.nextToken(); - do { - result = result + s; - if (tokenizer.hasMoreElements()) { - s = tokenizer.nextToken(); - } - } while (s.indexOf(quote) == -1); - String left = s.substring(0, s.indexOf(quote)); - String right = s.substring(s.indexOf(quote)); - result = result + left + "" + right; - } else { - // attribute is not quoted, it can only be one word - result = result + "" + s + ""; - } - } - } - } - result = result + ""; - return result; - } - - private static void buildTables(String iniFile) throws SAXException, IOException, ParserConfigurationException { - SAXBuilder builder = new SAXBuilder(); - builder.setEntityResolver(new Catalog(catalog)); - Document doc = builder.build(iniFile); - Element rt = doc.getRootElement(); - List tags = rt.getChildren("tag"); - - startsSegment = new Hashtable<>(); - translatableAttributes = new Hashtable<>(); - ignore = new Hashtable<>(); - ctypes = new Hashtable<>(); - keepFormating = new Hashtable<>(); - inline = new Hashtable<>(); - - Iterator i = tags.iterator(); - while (i.hasNext()) { - Element t = i.next(); - if (t.getAttributeValue("hard-break", "inline").equals("yes") - || t.getAttributeValue("hard-break", "inline").equals("segment")) { - startsSegment.put(t.getText(), "yes"); - } else if (t.getAttributeValue("hard-break", "inline").equals("no") - || t.getAttributeValue("hard-break", "inline").equals("inline")) { - inline.put(t.getText(), "yes"); - } else { - ignore.put(t.getText(), "yes"); - } - if (t.getAttributeValue("keep-format", "no").equals("yes")) { - keepFormating.put(t.getText(), "yes"); - } - String attributes = t.getAttributeValue("attributes", ""); - if (!attributes.equals("")) { - StringTokenizer tokenizer = new StringTokenizer(attributes, ";"); - int count = tokenizer.countTokens(); - Vector v = new Vector<>(count); - for (int j = 0; j < count; j++) { - v.add(tokenizer.nextToken()); - } - translatableAttributes.put(t.getText(), v); - } - String ctype = t.getAttributeValue("ctype", ""); - if (!ctype.equals("")) { - ctypes.put(t.getText(), ctype); - } - } - } - - private static void buildList() throws SAXException, IOException { - segments = new ArrayList<>(); - text = ""; - parseNode(root); - segments.add(text); - } - - private static void parseNode(XMLNode n) throws SAXException, IOException { - switch (n.getNodeType()) { - case XMLNode.ATTRIBUTE_NODE: - throw new SAXException("Parsed undeclared attribute node." + n); - case XMLNode.CDATA_SECTION_NODE: - String name = stack.peek(); - if (startsSegment.containsKey(name)) { - segments.add(text); - segments.add("" + '\u007F' + '\u007F' + ""); - } else { - segments.add(text); - segments.add("" + '\u007F' + '\u007F' + n.toString()); - } - translatable = ""; - text = ""; - break; - case XMLNode.COMMENT_NODE: - segments.add(text); - if (translateComments) { - segments.add("" + '\u007F' + '\u007F' + ""); - } else { - segments.add("" + '\u007F' + '\u007F' + n.toString()); - } - translatable = ""; - text = ""; - break; - case XMLNode.ELEMENT_NODE: - Element e = (Element) n; - if (dita_based && !isKnownElement(e.getName())) { - configureElement(e); - } - if (dita_based && e.getAttributeValue("translate", "yes").equals("no")) { - - if (startsSegment.containsKey(e.getName())) { - // treat as element to ignore, send to skeleton - segments.add(text); - if (e.getAttributeValue("removeTranslate", "no").equals("yes")) { - e.removeAttribute("translate"); - } - segments.add("" + '\u007F' + "" + '\u007F' + e.toString()); - text = ""; - translatable = ""; - stack = null; - stack = new Stack<>(); - return; - } - - removeComments(e); - - text = text + STARTG; - text = text + "<" + e.getName(); - List attributes = e.getAttributes(); - for (int i = 0; i < attributes.size(); i++) { - Attribute a = attributes.get(i); - text = text + " " + a.getName() + "=\"" + cleanAttribute(a.getValue()) + "\""; - } - text = text + ">" + STARTG; - List content = e.getContent(); - for (int i = 0; i < content.size(); i++) { - XMLNode node = content.get(i); - if (node.getNodeType() == XMLNode.TEXT_NODE) { - TextNode tn = (TextNode) node; - text = text + cleanString(tn.getText()); - } else { - Element el = (Element) node; - text = text + el.toString(); - } - } - text = text + ENDG + "" + ENDG; - - return; - } - if (dita_based && e.getAttributeValue("fluentaIgnore", "no").equals("yes")) { - e.removeAttribute("fluentaIgnore"); - segments.add(text); - segments.add("" + '\u007F' + "" + '\u007F' + e.toString()); - text = ""; - translatable = ""; - stack = null; - stack = new Stack<>(); - return; - } - if (startsSegment.containsKey(e.getName())) { - segments.add(text); - text = ""; - translatable = ""; - stack = null; - stack = new Stack<>(); - stack.push(e.getName()); - if (!keepFormating.containsKey(e.getName()) - && !e.getAttributeValue("xml:space", "default").equals("preserve")) { - normalizeElement(e); - } - } - if (ignore.containsKey(e.getName())) { - segments.add(text); - segments.add("" + '\u007F' + "" + '\u007F' + e.toString()); - text = ""; - translatable = ""; - stack = null; - stack = new Stack<>(); - return; - } - if (stack.size() == 0 && e.getChildren().size() == 0 && !translatableAttributes.containsKey(e.getName())) { - if (inline.containsKey(e.getName()) && !e.getText().equals("")) { - if (text.startsWith('\u007F' + "" + '\u007F')) { - segments.add(text); - text = ""; - translatable = ""; - stack = null; - stack = new Stack<>(); - } - stack.push(e.getName()); - if (!keepFormating.containsKey(e.getName()) - && !e.getAttributeValue("xml:space", "default").equals("preserve")) { - normalizeElement(e); - } - } else { - segments.add(text); - text = ""; - translatable = ""; - segments.add('\u007F' + "" + '\u007F' + e.toString()); - break; - } - } - if (stack.size() > 0 && !startsSegment.containsKey(e.getName())) { - stack.push(e.getName()); - } - List attributes = e.getAttributes(); - text = text + "<" + e.getName(); - if (attributes.size() > 0) { - for (int i = 0; i < attributes.size(); i++) { - Attribute a = attributes.get(i); - text = text + " " + a.getName() + "=\"" + cleanAttribute(a.getValue()) + "\""; - } - } - List content = e.getContent(); - if (content.size() == 0) { - if (text.equals("")) { - text = "" + '\u007F' + '\u007F' + "/>"; - } else { - text = text + "/>"; - } - } else { - if (!inline.containsKey(e.getName())) { - if (!text.equals("")) { - segments.add(text + ">"); - text = ""; - } else { - segments.add("" + '\u007F' + '\u007F' + ">"); - } - translatable = ""; - } else { - if (!text.equals("")) { - text = text + ">"; - } else { - segments.add("" + '\u007F' + '\u007F' + ">"); - translatable = ""; - } - } - for (int i = 0; i < content.size(); i++) { - parseNode(content.get(i)); - } - if (startsSegment.containsKey(e.getName())) { - segments.add(text); - text = ""; - translatable = ""; - } - if (!text.equals("")) { - text = text + ""; - } else { - segments.add("" + '\u007F' + '\u007F' + ""); - } - } - if (stack.size() > 0) { - stack.pop(); - } - - break; - case XMLNode.PROCESSING_INSTRUCTION_NODE: - if (inDesign && !translatable.trim().equals("")) { - text = text + n.toString(); - } else { - segments.add(text); - segments.add("" + '\u007F' + '\u007F' + n.toString()); - text = ""; - translatable = ""; - } - break; - case XMLNode.TEXT_NODE: - String value = ((TextNode) n).getText(); - // - // Don't enable replacement of "&". Replacement of "<" and ">" is needed because - // otherwise tag - // handling will fail (it searches for initial "<" and closing ">" - // - // value = value.replaceAll("&","&"); - value = value.replaceAll("<", "<"); - value = value.replaceAll(">", ">"); - - text = text + value; - if (value.trim().length() > 0) { - translatable = translatable + value; - } - if (value.trim().length() > 0 && text.startsWith("" + '\u007F' + '\u007F')) { - for (int j = 0; j < stack.size(); j++) { - if (startsSegment.containsKey(stack.get(j))) { - text = text.substring(2); - break; - } - } - } - - break; - default: - // ignore - break; - } - } - - private static void configureElement(Element e) { - String cls = e.getAttributeValue("class", ""); - String[] parts = cls.split("\\s"); - for (int h = 0; h < parts.length; h++) { - String part = parts[h]; - if (part.indexOf('/') == -1) { - continue; - } - String ancestor = part.substring(part.indexOf('/') + 1).trim(); - if (isKnownElement(ancestor)) { - if (startsSegment.containsKey(ancestor)) { - startsSegment.put(e.getName(), startsSegment.get(ancestor)); - } - if (inline.contains(ancestor)) { - inline.put(e.getName(), inline.get(ancestor)); - } - if (ignore.containsKey(ancestor)) { - ignore.put(e.getName(), ignore.get(ancestor)); - } - if (keepFormating.containsKey(ancestor)) { - keepFormating.put(e.getName(), keepFormating.get(ancestor)); - } - if (translatableAttributes.containsKey(ancestor)) { - translatableAttributes.put(e.getName(), translatableAttributes.get(ancestor)); - } - if (ctypes.containsKey(ancestor)) { - ctypes.put(e.getName(), ctypes.get(ancestor)); - } - return; - } - } - LOGGER.log(Level.WARNING, "Unknown element: " + e.getName()); - } - - private static boolean isKnownElement(String name) { - if (startsSegment.containsKey(name)) { - return true; - } - if (inline.containsKey(name)) { - return true; - } - if (ignore.containsKey(name)) { - return true; - } - return false; - } - - private static void removeComments(Element e) { - List content = new ArrayList<>(); - List list = e.getContent(); - Iterator it = list.iterator(); - while (it.hasNext()) { - XMLNode n = it.next(); - if (n.getNodeType() == XMLNode.ELEMENT_NODE) { - content.add(n); - } - if (n.getNodeType() == XMLNode.TEXT_NODE) { - content.add(n); - } - } - e.setContent(content); - } - - private static void normalizeElement(Element e) { - List l = e.getContent(); - Iterator i = l.iterator(); - List normal = new Vector<>(); - while (i.hasNext()) { - XMLNode n = i.next(); - if (n.getNodeType() == XMLNode.TEXT_NODE) { - String value = ((TextNode) n).getText(); - value = normalize(value); - ((TextNode) n).setText(value); - } - if (n.getNodeType() == XMLNode.ELEMENT_NODE) { - Element e1 = (Element) n; - if (!keepFormating.containsKey(e1.getName()) - && !e1.getAttributeValue("xml:space", "default").equals("preserve")) { - normalizeElement((Element) n); - } - } - normal.add(n); - } - e.setContent(normal); - } - - private static String cleanAttribute(String value) { - String result = value; - if (stack.size() > 1 && !text.startsWith("" + '\u007F' + '\u007F')) { - // this is an inline element and will be placed in - result = result.replaceAll("&", "###AMP###"); - } else { - result = result.replaceAll("&", "&"); - } - result = result.replaceAll(">", ">"); - result = result.replaceAll("<", "<"); - result = result.replaceAll("\"", """); - return result; - } - - private static String getType(String string) { - - String result = ""; - if (string.startsWith("') { - break; - } - result = result + c; - } - if (result.endsWith("/") && result.length() > 1) { - result = result.substring(0, result.length() - 1); - } - return result; - } - - public static String getEncoding(String fileName) throws IOException { - // check if there is a BOM (byte order mark) - // at the start of the document - byte[] array = new byte[2]; - try (FileInputStream inputStream = new FileInputStream(fileName)) { - if (inputStream.read(array) == -1) { - throw new IOException("Premature end of file"); - } - } - byte[] lt = "<".getBytes(); - byte[] feff = { -1, -2 }; - byte[] fffe = { -2, -1 }; - if (array[0] != lt[0]) { - // there is a BOM, now check the order - if (array[0] == fffe[0] && array[1] == fffe[1]) { - return StandardCharsets.UTF_16BE.name(); - } - if (array[0] == feff[0] && array[1] == feff[1]) { - return StandardCharsets.UTF_16LE.name(); - } - } - // check declared encoding - // return UTF-8 as default - String result = StandardCharsets.UTF_8.name(); - String line = ""; - try (FileReader in = new FileReader(fileName)) { - BufferedReader buffer = new BufferedReader(in); - line = buffer.readLine(); - } - if (line.startsWith("")); - line = line.replaceAll("\'", "\""); - StringTokenizer tokenizer = new StringTokenizer(line); - while (tokenizer.hasMoreTokens()) { - String token = tokenizer.nextToken(); - if (token.startsWith("encoding")) { - result = token.substring(token.indexOf('\"') + 1, token.lastIndexOf('\"')); - } - } - } - return result; - } - -} +/******************************************************************************* + * Copyright (c) 2003-2019 Maxprograms. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/org/documents/epl-v10.html + * + * Contributors: + * Maxprograms - initial API and implementation + *******************************************************************************/ +package com.maxprograms.converters.xml; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Stack; +import java.util.StringTokenizer; +import java.util.Vector; +import java.lang.System.Logger.Level; +import java.lang.System.Logger; + +import javax.xml.parsers.ParserConfigurationException; + +import org.xml.sax.SAXException; + +import com.maxprograms.converters.Constants; +import com.maxprograms.converters.Utils; +import com.maxprograms.segmenter.Segmenter; +import com.maxprograms.xml.Attribute; +import com.maxprograms.xml.CData; +import com.maxprograms.xml.Catalog; +import com.maxprograms.xml.Comment; +import com.maxprograms.xml.Document; +import com.maxprograms.xml.Element; +import com.maxprograms.xml.SAXBuilder; +import com.maxprograms.xml.SilentErrorHandler; +import com.maxprograms.xml.TextNode; +import com.maxprograms.xml.XMLNode; +import com.maxprograms.xml.XMLUtils; +import com.wutka.dtd.DTD; +import com.wutka.dtd.DTDParser; + +public class Xml2Xliff { + + private static final Logger LOGGER = System.getLogger(Xml2Xliff.class.getName()); + + private static final String STARTG = "%%START%%"; + private static final String ENDG = "%%END%%"; + + static final String DOUBLEPRIME = "\u2033"; + static final String MATHLT = "\u2039"; + static final String MATHGT = "\u200B\u203A"; + static final String GAMP = "\u200B\u203A"; + + private static String inputFile; + private static String skeletonFile; + private static String sourceLanguage; + private static String srcEncoding; + private static FileOutputStream output; + private static FileOutputStream skeleton; + private static int segId; + private static int tagId; + private static List segments; + private static Hashtable startsSegment; + private static Hashtable> translatableAttributes; + private static Hashtable inline; + private static Hashtable ctypes; + private static Hashtable keepFormating; + private static boolean segByElement; + private static Segmenter segmenter; + private static String catalog; + private static String rootElement; + private static Hashtable entities; + private static String entitiesMap; + private static Element root; + private static String text; + private static Stack stack; + private static String translatable = ""; + private static boolean inDesign = false; + private static Hashtable ignore; + private static boolean generic; + private static boolean resx; + private static String startText; + private static String endText; + private static boolean dita_based; + private static String targetLanguage; + private static boolean inCData; + private static boolean translateComments; + + private Xml2Xliff() { + // do not instantiate this class + // use run method instead + } + + public static Vector run(Hashtable params) { + Vector result = new Vector<>(); + segId = 1; + stack = new Stack<>(); + + inputFile = params.get("source"); + String xliffFile = params.get("xliff"); + skeletonFile = params.get("skeleton"); + sourceLanguage = params.get("srcLang"); + targetLanguage = params.get("tgtLang"); + srcEncoding = params.get("srcEncoding"); + catalog = params.get("catalog"); + String elementSegmentation = params.get("paragraph"); + String initSegmenter = params.get("srxFile"); + String isInDesign = params.get("InDesign"); + if (isInDesign != null) { + inDesign = true; + } else { + inDesign = false; + } + String isResx = params.get("resx"); + if (isResx != null) { + resx = true; + } else { + resx = false; + } + String dita = params.get("dita_based"); + if (dita != null) { + dita_based = dita.equalsIgnoreCase("yes"); + } + + String isGeneric = params.get("generic"); + if (isGeneric != null && isGeneric.equals("yes")) { + generic = true; + } else { + generic = false; + } + + String comments = params.get("translateComments"); + if (comments != null) { + translateComments = comments.equalsIgnoreCase("yes"); + } + + try { + boolean autoConfiguration = false; + String iniFile = getIniFile(inputFile); + File f = new File(iniFile); + if (!f.exists()) { + if (!generic) { + MessageFormat mf = new MessageFormat( + "Configuration file ''{0}'' not found. \\n\\nWrite a new configuration file for the XML Converter or set file type to ''XML (Generic)''."); + throw new IOException(mf.format(new Object[] { f.getName() })); + } + AutoConfiguration.run(inputFile, f.getAbsolutePath(), catalog); + autoConfiguration = true; + } + + if (elementSegmentation == null) { + segByElement = false; + } else { + if (elementSegmentation.equals("yes")) { + segByElement = true; + } else { + segByElement = false; + } + } + + if (segByElement == false) { + segmenter = new Segmenter(initSegmenter, sourceLanguage, catalog); + } + + String detected = getEncoding(inputFile); + if (!srcEncoding.equals(detected)) { + srcEncoding = detected; + } + + try (FileInputStream input = new FileInputStream(inputFile)) { + skeleton = new FileOutputStream(skeletonFile); + output = new FileOutputStream(xliffFile); + writeHeader(); + + int size = input.available(); + byte[] array = new byte[size]; + if (size != input.read(array)) { + result.add(Constants.ERROR); + result.add("Error reading from input file."); + return result; + } + String file = new String(array, srcEncoding); + // remove xml declaration and doctype + int begin = file.indexOf("<" + rootElement); + if (begin != -1) { + if (file.charAt(0) == '<') { + writeSkeleton(file.substring(0, begin)); + } else { + writeSkeleton(file.substring(1, begin)); + } + } + + buildTables(iniFile); + + if (autoConfiguration) { + Files.delete(Paths.get(f.toURI())); + } + + buildList(); + + processList(); + + skeleton.close(); + writeString("\n"); + writeString("\n"); + writeString(""); + } + output.close(); + + result.add(Constants.SUCCESS); + + } catch (IOException | SAXException | ParserConfigurationException e) { + LOGGER.log(Level.ERROR, "Error converting XML file", e); + result.add(Constants.ERROR); + result.add(e.getMessage()); + } + + return result; + } + + public static String getIniFile(String fileName) throws SAXException, IOException, ParserConfigurationException { + File folder = new File(System.getProperty("user.dir"), "xmlfilter"); + SAXBuilder builder = new SAXBuilder(); + Catalog cat = new Catalog(catalog); + builder.setEntityResolver(cat); + builder.setValidating(false); + builder.setErrorHandler(new SilentErrorHandler()); + /* + * set expandEntities to false if automatic inclusion of referenced documents is + * not desired. It is enabled by default, but may change later upon request. + * + */ + // builder.expandEntities(false); + Document doc = builder.build(fileName); + entities = new Hashtable<>(); + + Hashtable map = doc.getEntities(); + + entitiesMap = ""; + if (map != null) { + Enumeration en = entities.keys(); + while (en.hasMoreElements()) { + String key = en.nextElement(); + entitiesMap = entitiesMap + " " + + cleanEntity(entities.get(key)) + "\n"; + } + } + + // Add predefined standard entities + entities.put(">", ">"); + entities.put("<", "<"); + entities.put("&", "&"); + entities.put("'", "'"); + entities.put(""", "\""); + + root = doc.getRootElement(); + rootElement = root.getName(); + if (dita_based && rootElement.equals("svg")) { + dita_based = false; + } + + // check for ResX before anything else + // this requires a fixed ini name + if (root.getName().equals("root")) { + List dataElements = root.getChildren("data"); + if (dataElements.size() > 0) { + boolean isResx = false; + for (int i = 0; i < dataElements.size(); i++) { + Element g = (dataElements.get(i)).getChild("translate"); + if (g != null) { + isResx = true; + break; + } + } + if (isResx) { + return new File(folder, "config_resx.xml").getAbsolutePath(); + } + } + } + String pub = doc.getPublicId(); + String sys = doc.getSystemId(); + if (sys != null) { + // remove path from systemId + if (sys.indexOf('/') != -1 && sys.lastIndexOf('/') < sys.length()) { + sys = sys.substring(sys.lastIndexOf('/') + 1); + } + if (sys.indexOf('\\') != -1 && sys.lastIndexOf('/') < sys.length()) { + sys = sys.substring(sys.lastIndexOf('\\') + 1); + } + } + + Document d = builder.build(catalog); + Element r = d.getRootElement(); + List dtds = r.getChildren("dtd"); + Iterator i = dtds.iterator(); + while (i.hasNext()) { + Element dtd = i.next(); + if (pub != null && dtd.getAttributeValue("publicId", "").equals(pub)) { + String s = getRootElement(dtd.getText()); + if (s != null) { + return new File(folder, "config_" + s + ".xml").getAbsolutePath(); + } + } + if (sys != null && dtd.getAttributeValue("systemId", "").equals(sys)) { + String s = getRootElement(dtd.getText()); + if (s != null) { + return new File(folder, "config_" + s + ".xml").getAbsolutePath(); + } + if (dtd.getAttributeValue("systemId", "").endsWith(sys)) { + String st = getRootElement(dtd.getText()); + if (st != null) { + return new File(folder, "config_" + st + ".xml").getAbsolutePath(); + } + } + } + } + + if (rootElement.indexOf(':') != -1) { + return new File(folder, "config_" + rootElement.substring(0, rootElement.indexOf(':')) + ".xml") + .getAbsolutePath(); + } + + File f = new File(folder, "config_" + rootElement + ".xml"); + if (!f.exists() && dita_based) { + File base = new File(folder, "config_dita.xml"); + Document dd = builder.build(base); + List list = dd.getRootElement().getChildren(); + Iterator it = list.iterator(); + while (it.hasNext()) { + if (rootElement.equals(it.next().getText().trim())) { + return base.getAbsolutePath(); + } + } + String cls = root.getAttributeValue("class", ""); + String[] parts = cls.split("\\s"); + for (int h = 0; h < parts.length; h++) { + String part = parts[h]; + if (part.indexOf('/') == -1) { + continue; + } + String code = part.substring(part.indexOf('/') + 1).trim(); + it = list.iterator(); + while (it.hasNext()) { + if (code.equals(it.next().getText().trim())) { + return base.getAbsolutePath(); + } + } + } + } + return new File(folder, "config_" + rootElement + ".xml").getAbsolutePath(); + } + + private static String cleanEntity(String string) { + String result = string; + int control = result.indexOf('&'); + while (control != -1) { + int sc = result.indexOf(';', control); + if (sc == -1) { + // no semicolon, it's not an entity + result = result.substring(0, control) + "&" + result.substring(control + 1); + } else { + // may be an entity + String candidate = result.substring(control, sc) + ";"; + if (!candidate.equals("&")) { + String entity = entities.get(candidate); + if (entity != null) { + result = result.substring(0, control) + entity + result.substring(sc + 1); + } else if (candidate.startsWith("&#x")) { + // it's a character in hexadecimal format + int c = Integer.parseInt(candidate.substring(3, candidate.length() - 1), 16); + result = result.substring(0, control) + (char) c + result.substring(sc + 1); + } else if (candidate.startsWith("&#")) { + // it's a character + int c = Integer.parseInt(candidate.substring(2, candidate.length() - 1)); + result = result.substring(0, control) + (char) c + result.substring(sc + 1); + } else { + result = result.substring(0, control) + "&" + result.substring(control + 1); + } + } + } + if (control < result.length()) { + control++; + } + control = result.indexOf('&', control); + } + + control = result.indexOf('<'); + while (control != -1) { + result = result.substring(0, control) + "<" + result.substring(control + 1); + if (control < result.length()) { + control++; + } + control = result.indexOf('<', control); + } + + control = result.indexOf('>'); + while (control != -1) { + result = result.substring(0, control) + ">" + result.substring(control + 1); + if (control < result.length()) { + control++; + } + control = result.indexOf('>', control); + } + return result; + } + + private static String getRootElement(String file) { + String result = null; + File dtd = new File(file); + try { + DTDParser parser = new DTDParser(dtd); + DTD d = parser.parse(true); + if (d != null && d.rootElement != null) { + result = d.rootElement.getName(); + } + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Error getting root element from DTD", e); + } + return result; + } + + private static void writeHeader() throws IOException { + String tgtLang = ""; + if (targetLanguage != null) { + tgtLang = "\" target-language=\"" + targetLanguage; + } + + String format = "xml"; + if (inDesign) { + format = "x-inx"; + } else if (resx) { + format = "resx"; + } + writeString("\n"); + writeString("\n"); + writeString("\n"); + + writeString("\n"); + writeString("
\n"); + writeString(" \n"); + writeString(" \n"); + writeString(" \n"); + if (!entitiesMap.equals("")) { + writeString(" \n" + entitiesMap + " \n"); + } + writeString("
\n"); + writeString("\n"); + } + + private static void processList() throws IOException, SAXException, ParserConfigurationException { + for (int i = 0; i < segments.size(); i++) { + String txt = segments.get(i); + + if (txt.startsWith("" + '\u007F' + "" + '\u007F')) { + // send directly to skeleton + writeSkeleton(txt.substring(2)); + continue; + } + if (txt.startsWith("" + '\u0081')) { + inCData = true; + txt = txt.substring(1); + } else { + inCData = false; + } + if (inDesign && !txt.trim().equals("")) { + if (txt.startsWith("c_") && !txt.substring(2).trim().equals("")) { + writeSkeleton("c_"); + txt = txt.substring(2); + txt = txt.replaceAll("~sep~", "_"); + } else { + writeSkeleton(txt); + continue; + } + } + tagId = 0; + if (dita_based) { + txt = prepareG(txt); + } + txt = addTags(txt); + if (segByElement == true) { + writeSegment(txt); + } else { + String[] segs = segmenter.segment(txt); + for (int h = 0; h < segs.length; h++) { + String seg = segs[h]; + while (seg.startsWith("" + '\u2029')) { + writeSkeleton("" + '\u2029'); + seg = seg.substring(1); + } + writeSegment(seg); + } + } + } + } + + private static String prepareG(String string) { + int start = string.indexOf(STARTG); + if (start == -1) { + return string; + } + String txt = string; + String result = txt.substring(0, start); + while (start != -1) { + txt = txt.substring(start + STARTG.length()); + start = txt.indexOf(STARTG); + String element = txt.substring(0, start); + result = result + makeMrk(element); + txt = txt.substring(start + STARTG.length()); + int end = txt.indexOf(ENDG); + String content = txt.substring(0, end); + result = result + content + ""; + end = txt.indexOf(ENDG, end + 1); + txt = txt.substring(end + ENDG.length()); + start = txt.indexOf(STARTG); + if (start != -1) { + result = result + txt.substring(0, start); + } + } + return result + txt; + } + + private static void writeSegment(String tagged) throws IOException, SAXException, ParserConfigurationException { + String restype = ""; + if (!containsText(tagged)) { + String untagged = removeTags(tagged); + writeSkeleton(untagged); + return; + } + if (inCData) { + restype = " restype=\"x-cdata\""; + } + String seg = " \n" + + " " + tagged + "\n \n"; + + String clean = tidy(seg); + String dirt = startText + "%%%" + segId++ + "%%%\n" + endText; + writeString(clean); + writeSkeleton(dirt); + } + + private static String removeTags(String tagged) throws IOException, SAXException, ParserConfigurationException { + String source = "" + tagged + ""; + SAXBuilder b = new SAXBuilder(); + Document d = null; + try { + d = b.build(new ByteArrayInputStream(source.getBytes(StandardCharsets.UTF_8))); + } catch (SAXException sax) { + LOGGER.log(Level.ERROR, "Broken segment: " + source); + throw sax; + } + Element r = d.getRootElement(); + return extractText(r); + } + + private static String extractText(Element element) throws SAXException { + if (element.getName().equals("ph")) { + return Xliff2Xml.fixEntities(element); + } + if (dita_based && element.getName().equals("g")) { + return cleanMrk(element); + } + + String result = ""; + List content = element.getContent(); + Iterator i = content.iterator(); + while (i.hasNext()) { + XMLNode n = i.next(); + switch (n.getNodeType()) { + case XMLNode.ELEMENT_NODE: + Element e = (Element) n; + if (e.getName().equals("ph")) { + String ph = extractText(e); + result = result + ph; + } else if (e.getName().equals("mrk")) { + String mrk = cleanMrk(e); + result = result + mrk; + } else { + throw new SAXException("broken tagged text"); + } + break; + case XMLNode.TEXT_NODE: + if (inCData) { + result = result + ((TextNode) n).getText(); + } else { + result = result + addEntities(((TextNode) n).getText()); + } + break; + default: + // ignore + break; + } + } + return result; + } + + private static String cleanMrk(Element element) throws SAXException { + String ts = element.getAttributeValue("ts", ""); + if (ts.isEmpty()) { + throw new SAXException("Broken element."); + } + ts = restoreChars(ts).trim(); + String name = ""; + for (int i = 1; i < ts.length(); i++) { + if (Character.isSpaceChar(ts.charAt(i))) { + break; + } + name = name + ts.charAt(i); + } + String content = ""; + List nodes = element.getContent(); + Iterator it = nodes.iterator(); + while (it.hasNext()) { + XMLNode n = it.next(); + switch (n.getNodeType()) { + case XMLNode.ELEMENT_NODE: + Element e = (Element) n; + String ph = extractText(e); + content = content + ph; + break; + case XMLNode.TEXT_NODE: + content = content + XMLUtils.cleanText(((TextNode) n).getText()); + break; + default: + // ignore + break; + } + } + return ts + content + ""; // TODO recurse content + } + + private static String restoreChars(String string) { + String result = string.replaceAll(Xml2Xliff.MATHLT, "<"); + result = result.replaceAll(Xml2Xliff.MATHGT, ">"); + result = result.replaceAll(Xml2Xliff.DOUBLEPRIME, "\""); + result = result.replaceAll(Xml2Xliff.GAMP, "&"); + return result; + } + + private static String addEntities(String string) { + String result = string.replaceAll("<", "<"); + result = result.replaceAll(">", ">"); + result = result.replaceAll(""", "\""); + result = result.replaceAll("&", "&"); + return result; + } + + private static String tidy(String seg) throws SAXException, IOException, ParserConfigurationException { + startText = ""; + endText = ""; + SAXBuilder b = new SAXBuilder(); + Document d = b.build(new ByteArrayInputStream(seg.getBytes(StandardCharsets.UTF_8))); + Element r = d.getRootElement(); + Element s = r.getChild("source"); + if (s.getChildren().size() == 0) { + return seg; + } + List start = new ArrayList<>(); + List end = new ArrayList<>(); + List txt = new ArrayList<>(); + + List content = s.getContent(); + + Vector startTags = new Vector<>(); + Vector endTags = new Vector<>(); + + for (int i = 0; i < content.size(); i++) { + XMLNode n = content.get(i); + if (n.getNodeType() == XMLNode.TEXT_NODE && !n.toString().trim().equals("")) { + break; + } + if (n.getNodeType() == XMLNode.ELEMENT_NODE) { + Element e = (Element) n; + if (!e.getName().equals("ph")) { + break; + } + startTags.add(e); + } + start.add(n); + } + + if (startTags.size() == 0) { + start.clear(); + } + for (int i = content.size() - 1; i >= 0; i--) { + XMLNode n = content.get(i); + if (n.getNodeType() == XMLNode.TEXT_NODE && !n.toString().trim().equals("")) { + break; + } + if (n.getNodeType() == XMLNode.ELEMENT_NODE) { + Element e = (Element) n; + if (!e.getName().equals("ph")) { + break; + } + endTags.add(0, e); + } + end.add(0, n); + } + if (endTags.size() == 0) { + end.clear(); + } + + int trimmed = 0; + + if (startTags.size() > 0 && endTags.size() > 0) { + for (int i = 0; i < startTags.size() && i < endTags.size(); i++) { + Element f = (Element) startTags.get(i); + Element l = (Element) endTags.get(endTags.size() - 1 - i); + if ((l.getText().startsWith("")) + && (!(f.getText().startsWith("")))) { + String endTag = l.getText().substring(2); + endTag = endTag.substring(0, endTag.length() - 1); + if (f.getText().startsWith("<" + endTag)) { + // matched + trimmed++; + } + } + } + } + + if (trimmed > 0) { + List start2 = new ArrayList<>(); + List end2 = new ArrayList<>(); + + int count = 0; + for (int h = 0; h < start.size(); h++) { + XMLNode n = start.get(h); + start2.add(n); + if (n.getNodeType() == XMLNode.ELEMENT_NODE) { + count++; + if (count == trimmed) { + break; + } + } + } + start = start2; + count = 0; + for (int h = end.size() - 1; h >= 0; h--) { + XMLNode n = end.get(h); + end2.add(0, n); + if (n.getNodeType() == XMLNode.ELEMENT_NODE) { + count++; + if (count == trimmed) { + break; + } + } + } + end = end2; + } else { + if (startTags.size() == 1 && s.getChildren().size() == 1) { + // send initial tag to skeleton, keep end spaces + end.clear(); + } else if (s.getChildren().size() == 1 && endTags.size() == 1) { + // set ending tag to skeleton, keep initial spaces + start.clear(); + } else { + start.clear(); + end.clear(); + } + } + + for (int i = start.size(); i < content.size() - end.size(); i++) { + txt.add(content.get(i)); + } + + for (int i = 0; i < start.size(); i++) { + XMLNode n = start.get(i); + if (n.getNodeType() == XMLNode.TEXT_NODE) { + startText += ((TextNode) n).getText(); + } + if (n.getNodeType() == XMLNode.ELEMENT_NODE) { + startText += ((Element) n).getText(); + } + } + for (int i = 0; i < end.size(); i++) { + XMLNode n = end.get(i); + if (n.getNodeType() == XMLNode.TEXT_NODE) { + endText += ((TextNode) n).getText(); + } + if (n.getNodeType() == XMLNode.ELEMENT_NODE) { + endText += ((Element) n).getText(); + } + } + s.setContent(txt); + List children = s.getChildren("ph"); + for (int id = 0; id < children.size(); id++) { + Element child = children.get(id); + child.setAttribute("id", "" + id); + } + + return r.toString(); + } + + private static boolean containsText(String string) { + String tagged = string; + int start = tagged.indexOf(""); + if (dita_based) { + while (start != -1 && end != -1) { + tagged = tagged.substring(0, start) + tagged.substring(end + 6); + start = tagged.indexOf("", start + 5); + } else { + end = -1; + } + } + } + start = tagged.indexOf(""); + + while (start != -1 && end != -1) { + tagged = tagged.substring(0, start) + tagged.substring(end + 5); + start = tagged.indexOf("", start + 4); + } else { + end = -1; + } + } + + tagged = tagged.trim(); + if (tagged.length() == 0) { + return false; + } + for (int i = 0; i < tagged.length(); i++) { + int c = tagged.charAt(i); + if (" \u00A0\r\n\f\t\u2028\u2029,.;\":<>¿?¡!()[]{}=+/*\u00AB\u00BB\u201C\u201D\u201E\uFF00" + .indexOf(c) == -1) { + return true; + } + } + return false; + } + + private static String normalize(String string) { + String result = string.replace('\n', ' '); + result = result.replace('\t', ' '); + result = result.replace('\r', ' '); + result = result.replace('\f', ' '); + String rs = ""; + int length = result.length(); + for (int i = 0; i < length; i++) { + char ch = result.charAt(i); + if (ch != ' ') { + rs = rs + ch; + } else { + rs = rs + ch; + while (i < (length - 1) && result.charAt(i + 1) == ' ') { + i++; + } + } + } + return rs; + } + + private static String addTags(String string) { + String src = string; + String result = ""; + int start = src.indexOf('<'); + int end = src.indexOf('>'); + + while (start != -1) { + if (start > 0) { + result = result + cleanString(src.substring(0, start)); + src = src.substring(start); + start = src.indexOf('<'); + end = src.indexOf('>'); + } + String element = src.substring(start, end + 1); + src = src.substring(end + 1); + if (dita_based) { + if (!(element.startsWith(""))) { + result = result + tag(element); + } else { + result = result + element; + } + } else { + result = result + tag(element); + } + start = src.indexOf('<'); + end = src.indexOf('>'); + } + result = result + cleanString(src); + return result; + } + + private static String makeMrk(String element) { + return ""; + } + + private static String clean(String string) { + String result = string.replaceAll("<", MATHLT); + result = result.replaceAll(">", MATHGT); + result = result.replaceAll("\"", DOUBLEPRIME); + return replaceAmp(result); + } + + private static String replaceAmp(String value) { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (c == '&') { + result.append(GAMP); + } else { + result.append(c); + } + } + return result.toString(); + } + + private static String tag(String element) { + String result = ""; + String type = getType(element); + if (translatableAttributes.containsKey(type)) { + result = extractAttributes(type, element); + } else { + String ctype = ""; + if (ctypes.containsKey(type)) { + ctype = " ctype=\"" + ctypes.get(type) + "\""; + } + result = "" + cleanString(element) + ""; + } + return result; + } + + private static String cleanString(String string) { + String s = string; + int control = s.indexOf('&'); + while (control != -1) { + int sc = s.indexOf(";", control); + if (sc == -1) { + // no semicolon, it's not an entity + s = s.substring(0, control) + "&" + s.substring(control + 1); + } else { + // may be an entity + String candidate = s.substring(control, sc) + ";"; + if (validEntitiy(candidate)) { + if (!candidate.equals("&") && !candidate.equals(""")) { + String entity = entities.get(candidate); + if (entity != null) { + s = s.substring(0, control) + entity + s.substring(sc + 1); + } else { + s = s.substring(0, control) + "%%%ph id=\"" + tagId++ + "\"%%%&" + + candidate.substring(1) + "%%%/ph%%%" + s.substring(sc + 1); + } + } else { + // it is an "&" + s = s.substring(0, control) + "&" + s.substring(control + 1); + } + } else { + // treat as an "&" + s = s.substring(0, control) + "&" + s.substring(control + 1); + } + } + if (control < s.length()) { + control++; + } + control = s.indexOf('&', control); + } + + control = s.indexOf('<'); + while (control != -1) { + s = s.substring(0, control) + "<" + s.substring(control + 1); + if (control < s.length()) { + control++; + } + control = s.indexOf('<', control); + } + + control = s.indexOf('>'); + while (control != -1) { + s = s.substring(0, control) + ">" + s.substring(control + 1); + if (control < s.length()) { + control++; + } + control = s.indexOf('>', control); + } + s = s.replaceAll("%%%/ph%%%", "
"); + s = s.replaceAll("%%%ph", "&"); + return XMLUtils.validChars(s); + } + + private static boolean validEntitiy(String candidate) { + if (candidate.length() < 3) { + return false; + } + char nameStart = candidate.charAt(1); + // ":" | [A-Z] | "_" | [a-z] | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF] | + // [#x370-#x37D] | [#x37F-#x1FFF] | [#x200C-#x200D] | [#x2070-#x218F] | + // [#x2C00-#x2FEF] | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] | + // [#x10000-#xEFFFF] + if (nameStart == ':' || (nameStart >= 'A' && nameStart <= 'Z') || (nameStart >= 'a' && nameStart <= 'z') + || (nameStart >= '\u00C0' && nameStart <= '\u00D6') || (nameStart >= '\u00D8' && nameStart <= '\u00F6') + || (nameStart >= '\u00F8' && nameStart <= '\u02FF') || (nameStart >= '\u0370' && nameStart <= '\u037D') + || (nameStart >= '\u037F' && nameStart <= '\u1FFF') || (nameStart >= '\u200C' && nameStart <= '\u200D') + || (nameStart >= '\u2017' && nameStart <= '\u218F') || (nameStart >= '\u2C00' && nameStart <= '\u2FEF') + || (nameStart >= '\u3001' && nameStart <= '\uD7FF') || (nameStart >= '\uF900' && nameStart <= '\uFDCF') + || (nameStart >= '\uFDF0' && nameStart <= '\uFFFD')) // not considered [#x10000-#xEFFFF] + { + // its OK + } else { + return false; + } + for (int i = 2; i < candidate.length() - 1; i++) { + // valid = nameStart | "-" | "." | [0-9] | #xB7 | [#x0300-#x036F] | + // [#x203F-#x2040] + char nameChar = candidate.charAt(i); + if (nameChar == ':' || (nameChar >= 'A' && nameChar <= 'Z') || (nameChar >= 'a' && nameChar <= 'z') + || (nameChar >= '\u00C0' && nameChar <= '\u00D6') || (nameChar >= '\u00D8' && nameChar <= '\u00F6') + || (nameChar >= '\u00F8' && nameChar <= '\u02FF') || (nameChar >= '\u0370' && nameChar <= '\u037D') + || (nameChar >= '\u037F' && nameChar <= '\u1FFF') || (nameChar >= '\u200C' && nameChar <= '\u200D') + || (nameChar >= '\u2017' && nameChar <= '\u218F') || (nameChar >= '\u2C00' && nameChar <= '\u2FEF') + || (nameChar >= '\u3001' && nameChar <= '\uD7FF') || (nameChar >= '\uF900' && nameChar <= '\uFDCF') + || (nameChar >= '\uFDF0' && nameChar <= '\uFFFD') || nameChar == '-' + || (nameChar >= '0' && nameChar <= '9') || (nameChar >= '\u0300' && nameChar <= '\u036F') + || (nameChar >= '\u203F' && nameChar <= '\u2040')) { + // its OK + } else { + return false; + } + } + return true; + } + + private static void writeSkeleton(String string) throws IOException { + skeleton.write(string.getBytes(StandardCharsets.UTF_8)); + } + + private static void writeString(String string) throws IOException { + output.write(string.getBytes(StandardCharsets.UTF_8)); + } + + private static String extractAttributes(String type, String element) { + + String ctype = ""; + if (ctypes.containsKey(type)) { + ctype = " ctype=\"" + ctypes.get(type) + "\""; + } + String result = ""; + String clean = cleanString(element); + + Vector v = translatableAttributes.get(type); + + StringTokenizer tokenizer = new StringTokenizer(clean, "&= \t\n\r\f/", true); + + while (tokenizer.hasMoreTokens()) { + String token = tokenizer.nextToken(); + if (!v.contains(token)) { + result = result + token; + } else { + result = result + token; + String s = tokenizer.nextToken(); + while (s.equals("=") || s.equals(" ")) { + result = result + s; + s = tokenizer.nextToken(); + } + // s contains the first word of the attribute + if (((s.startsWith("\"") && s.endsWith("\"")) || (s.startsWith("'") && s.endsWith("'"))) + && s.length() > 1) { + // the value is one word and it is quoted + result = result + s.substring(0, 1) + "" + s.substring(1, s.length() - 1) + "" + s.substring(s.length() - 1); + } else { + if (s.startsWith("\"") || s.startsWith("'")) { + // attribute value is quoted, but it has more than one + // word + String quote = s.substring(0, 1); + result = result + s.substring(0, 1) + "" + s.substring(1); + s = tokenizer.nextToken(); + do { + result = result + s; + if (tokenizer.hasMoreElements()) { + s = tokenizer.nextToken(); + } + } while (s.indexOf(quote) == -1); + String left = s.substring(0, s.indexOf(quote)); + String right = s.substring(s.indexOf(quote)); + result = result + left + "" + right; + } else { + // attribute is not quoted, it can only be one word + result = result + "" + s + ""; + } + } + } + } + result = result + ""; + return result; + } + + private static void buildTables(String iniFile) throws SAXException, IOException, ParserConfigurationException { + SAXBuilder builder = new SAXBuilder(); + builder.setEntityResolver(new Catalog(catalog)); + Document doc = builder.build(iniFile); + Element rt = doc.getRootElement(); + List tags = rt.getChildren("tag"); + + startsSegment = new Hashtable<>(); + translatableAttributes = new Hashtable<>(); + ignore = new Hashtable<>(); + ctypes = new Hashtable<>(); + keepFormating = new Hashtable<>(); + inline = new Hashtable<>(); + + Iterator i = tags.iterator(); + while (i.hasNext()) { + Element t = i.next(); + if (t.getAttributeValue("hard-break", "inline").equals("yes") + || t.getAttributeValue("hard-break", "inline").equals("segment")) { + startsSegment.put(t.getText(), "yes"); + } else if (t.getAttributeValue("hard-break", "inline").equals("no") + || t.getAttributeValue("hard-break", "inline").equals("inline")) { + inline.put(t.getText(), "yes"); + } else { + ignore.put(t.getText(), "yes"); + } + if (t.getAttributeValue("keep-format", "no").equals("yes")) { + keepFormating.put(t.getText(), "yes"); + } + String attributes = t.getAttributeValue("attributes", ""); + if (!attributes.equals("")) { + StringTokenizer tokenizer = new StringTokenizer(attributes, ";"); + int count = tokenizer.countTokens(); + Vector v = new Vector<>(count); + for (int j = 0; j < count; j++) { + v.add(tokenizer.nextToken()); + } + translatableAttributes.put(t.getText(), v); + } + String ctype = t.getAttributeValue("ctype", ""); + if (!ctype.equals("")) { + ctypes.put(t.getText(), ctype); + } + } + } + + private static void buildList() throws SAXException, IOException { + segments = new ArrayList<>(); + text = ""; + parseNode(root); + segments.add(text); + } + + private static void parseNode(XMLNode n) throws SAXException, IOException { + switch (n.getNodeType()) { + case XMLNode.ATTRIBUTE_NODE: + throw new SAXException("Parsed undeclared attribute node." + n); + case XMLNode.CDATA_SECTION_NODE: + String name = stack.peek(); + if (startsSegment.containsKey(name)) { + segments.add(text); + segments.add("" + '\u007F' + '\u007F' + ""); + } else { + segments.add(text); + segments.add("" + '\u007F' + '\u007F' + n.toString()); + } + translatable = ""; + text = ""; + break; + case XMLNode.COMMENT_NODE: + segments.add(text); + if (translateComments) { + segments.add("" + '\u007F' + '\u007F' + ""); + } else { + segments.add("" + '\u007F' + '\u007F' + n.toString()); + } + translatable = ""; + text = ""; + break; + case XMLNode.ELEMENT_NODE: + Element e = (Element) n; + if (dita_based && !isKnownElement(e.getName())) { + configureElement(e); + } + if (dita_based && e.getAttributeValue("translate", "yes").equals("no")) { + + if (startsSegment.containsKey(e.getName())) { + // treat as element to ignore, send to skeleton + segments.add(text); + if (e.getAttributeValue("removeTranslate", "no").equals("yes")) { + e.removeAttribute("translate"); + } + segments.add("" + '\u007F' + "" + '\u007F' + e.toString()); + text = ""; + translatable = ""; + stack = null; + stack = new Stack<>(); + return; + } + + removeComments(e); + + text = text + STARTG; + text = text + "<" + e.getName(); + List attributes = e.getAttributes(); + for (int i = 0; i < attributes.size(); i++) { + Attribute a = attributes.get(i); + text = text + " " + a.getName() + "=\"" + cleanAttribute(a.getValue()) + "\""; + } + text = text + ">" + STARTG; + List content = e.getContent(); + for (int i = 0; i < content.size(); i++) { + XMLNode node = content.get(i); + if (node.getNodeType() == XMLNode.TEXT_NODE) { + TextNode tn = (TextNode) node; + text = text + cleanString(tn.getText()); + } else { + Element el = (Element) node; + text = text + el.toString(); + } + } + text = text + ENDG + "" + ENDG; + + return; + } + if (dita_based && e.getAttributeValue("fluentaIgnore", "no").equals("yes")) { + e.removeAttribute("fluentaIgnore"); + segments.add(text); + segments.add("" + '\u007F' + "" + '\u007F' + e.toString()); + text = ""; + translatable = ""; + stack = null; + stack = new Stack<>(); + return; + } + if (startsSegment.containsKey(e.getName())) { + segments.add(text); + text = ""; + translatable = ""; + stack = null; + stack = new Stack<>(); + stack.push(e.getName()); + if (!keepFormating.containsKey(e.getName()) + && !e.getAttributeValue("xml:space", "default").equals("preserve")) { + normalizeElement(e); + } + } + if (ignore.containsKey(e.getName())) { + segments.add(text); + segments.add("" + '\u007F' + "" + '\u007F' + e.toString()); + text = ""; + translatable = ""; + stack = null; + stack = new Stack<>(); + return; + } + if (stack.size() == 0 && e.getChildren().size() == 0 && !translatableAttributes.containsKey(e.getName())) { + if (inline.containsKey(e.getName()) && !e.getText().equals("")) { + if (text.startsWith('\u007F' + "" + '\u007F')) { + segments.add(text); + text = ""; + translatable = ""; + stack = null; + stack = new Stack<>(); + } + stack.push(e.getName()); + if (!keepFormating.containsKey(e.getName()) + && !e.getAttributeValue("xml:space", "default").equals("preserve")) { + normalizeElement(e); + } + } else { + segments.add(text); + text = ""; + translatable = ""; + segments.add('\u007F' + "" + '\u007F' + e.toString()); + break; + } + } + if (stack.size() > 0 && !startsSegment.containsKey(e.getName())) { + stack.push(e.getName()); + } + List attributes = e.getAttributes(); + text = text + "<" + e.getName(); + if (attributes.size() > 0) { + for (int i = 0; i < attributes.size(); i++) { + Attribute a = attributes.get(i); + text = text + " " + a.getName() + "=\"" + cleanAttribute(a.getValue()) + "\""; + } + } + List content = e.getContent(); + if (content.size() == 0) { + if (text.equals("")) { + text = "" + '\u007F' + '\u007F' + "/>"; + } else { + text = text + "/>"; + } + } else { + if (!inline.containsKey(e.getName())) { + if (!text.equals("")) { + segments.add(text + ">"); + text = ""; + } else { + segments.add("" + '\u007F' + '\u007F' + ">"); + } + translatable = ""; + } else { + if (!text.equals("")) { + text = text + ">"; + } else { + segments.add("" + '\u007F' + '\u007F' + ">"); + translatable = ""; + } + } + for (int i = 0; i < content.size(); i++) { + parseNode(content.get(i)); + } + if (startsSegment.containsKey(e.getName())) { + segments.add(text); + text = ""; + translatable = ""; + } + if (!text.equals("")) { + text = text + ""; + } else { + segments.add("" + '\u007F' + '\u007F' + ""); + } + } + if (stack.size() > 0) { + stack.pop(); + } + + break; + case XMLNode.PROCESSING_INSTRUCTION_NODE: + if (inDesign && !translatable.trim().equals("")) { + text = text + n.toString(); + } else { + segments.add(text); + segments.add("" + '\u007F' + '\u007F' + n.toString()); + text = ""; + translatable = ""; + } + break; + case XMLNode.TEXT_NODE: + String value = ((TextNode) n).getText(); + // + // Don't enable replacement of "&". Replacement of "<" and ">" is needed because + // otherwise tag + // handling will fail (it searches for initial "<" and closing ">" + // + // value = value.replaceAll("&","&"); + value = value.replaceAll("<", "<"); + value = value.replaceAll(">", ">"); + + text = text + value; + if (value.trim().length() > 0) { + translatable = translatable + value; + } + if (value.trim().length() > 0 && text.startsWith("" + '\u007F' + '\u007F')) { + for (int j = 0; j < stack.size(); j++) { + if (startsSegment.containsKey(stack.get(j))) { + text = text.substring(2); + break; + } + } + } + + break; + default: + // ignore + break; + } + } + + private static void configureElement(Element e) { + String cls = e.getAttributeValue("class", ""); + String[] parts = cls.split("\\s"); + for (int h = 0; h < parts.length; h++) { + String part = parts[h]; + if (part.indexOf('/') == -1) { + continue; + } + String ancestor = part.substring(part.indexOf('/') + 1).trim(); + if (isKnownElement(ancestor)) { + if (startsSegment.containsKey(ancestor)) { + startsSegment.put(e.getName(), startsSegment.get(ancestor)); + } + if (inline.contains(ancestor)) { + inline.put(e.getName(), inline.get(ancestor)); + } + if (ignore.containsKey(ancestor)) { + ignore.put(e.getName(), ignore.get(ancestor)); + } + if (keepFormating.containsKey(ancestor)) { + keepFormating.put(e.getName(), keepFormating.get(ancestor)); + } + if (translatableAttributes.containsKey(ancestor)) { + translatableAttributes.put(e.getName(), translatableAttributes.get(ancestor)); + } + if (ctypes.containsKey(ancestor)) { + ctypes.put(e.getName(), ctypes.get(ancestor)); + } + return; + } + } + LOGGER.log(Level.WARNING, "Unknown element: " + e.getName()); + } + + private static boolean isKnownElement(String name) { + if (startsSegment.containsKey(name)) { + return true; + } + if (inline.containsKey(name)) { + return true; + } + if (ignore.containsKey(name)) { + return true; + } + return false; + } + + private static void removeComments(Element e) { + List content = new ArrayList<>(); + List list = e.getContent(); + Iterator it = list.iterator(); + while (it.hasNext()) { + XMLNode n = it.next(); + if (n.getNodeType() == XMLNode.ELEMENT_NODE) { + content.add(n); + } + if (n.getNodeType() == XMLNode.TEXT_NODE) { + content.add(n); + } + } + e.setContent(content); + } + + private static void normalizeElement(Element e) { + List l = e.getContent(); + Iterator i = l.iterator(); + List normal = new Vector<>(); + while (i.hasNext()) { + XMLNode n = i.next(); + if (n.getNodeType() == XMLNode.TEXT_NODE) { + String value = ((TextNode) n).getText(); + value = normalize(value); + ((TextNode) n).setText(value); + } + if (n.getNodeType() == XMLNode.ELEMENT_NODE) { + Element e1 = (Element) n; + if (!keepFormating.containsKey(e1.getName()) + && !e1.getAttributeValue("xml:space", "default").equals("preserve")) { + normalizeElement((Element) n); + } + } + normal.add(n); + } + e.setContent(normal); + } + + private static String cleanAttribute(String value) { + String result = value; + if (stack.size() > 1 && !text.startsWith("" + '\u007F' + '\u007F')) { + // this is an inline element and will be placed in + result = result.replaceAll("&", "###AMP###"); + } else { + result = result.replaceAll("&", "&"); + } + result = result.replaceAll(">", ">"); + result = result.replaceAll("<", "<"); + result = result.replaceAll("\"", """); + return result; + } + + private static String getType(String string) { + + String result = ""; + if (string.startsWith("') { + break; + } + result = result + c; + } + if (result.endsWith("/") && result.length() > 1) { + result = result.substring(0, result.length() - 1); + } + return result; + } + + public static String getEncoding(String fileName) throws IOException { + // check if there is a BOM (byte order mark) + // at the start of the document + byte[] array = new byte[2]; + try (FileInputStream inputStream = new FileInputStream(fileName)) { + if (inputStream.read(array) == -1) { + throw new IOException("Premature end of file"); + } + } + byte[] lt = "<".getBytes(); + byte[] feff = { -1, -2 }; + byte[] fffe = { -2, -1 }; + if (array[0] != lt[0]) { + // there is a BOM, now check the order + if (array[0] == fffe[0] && array[1] == fffe[1]) { + return StandardCharsets.UTF_16BE.name(); + } + if (array[0] == feff[0] && array[1] == feff[1]) { + return StandardCharsets.UTF_16LE.name(); + } + } + // check declared encoding + // return UTF-8 as default + String result = StandardCharsets.UTF_8.name(); + String line = ""; + try (FileReader in = new FileReader(fileName)) { + BufferedReader buffer = new BufferedReader(in); + line = buffer.readLine(); + } + if (line.startsWith("")); + line = line.replaceAll("\'", "\""); + StringTokenizer tokenizer = new StringTokenizer(line); + while (tokenizer.hasMoreTokens()) { + String token = tokenizer.nextToken(); + if (token.startsWith("encoding")) { + result = token.substring(token.indexOf('\"') + 1, token.lastIndexOf('\"')); + } + } + } + return result; + } + +} diff --git a/src/com/maxprograms/server/FilterServer.java b/src/com/maxprograms/server/FilterServer.java index b441ae17..dcbca965 100644 --- a/src/com/maxprograms/server/FilterServer.java +++ b/src/com/maxprograms/server/FilterServer.java @@ -1,600 +1,669 @@ -/******************************************************************************* - * Copyright (c) 2003-2019 Maxprograms. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 1.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/org/documents/epl-v10.html - * - * Contributors: - * Maxprograms - initial API and implementation - *******************************************************************************/ - -package com.maxprograms.server; - -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.lang.System.Logger; -import java.lang.System.Logger.Level; -import java.net.InetSocketAddress; -import java.net.URI; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.List; -import java.util.Set; -import java.util.TreeMap; -import java.util.Vector; - -import javax.xml.parsers.ParserConfigurationException; - -import org.json.JSONArray; -import org.json.JSONObject; -import org.xml.sax.SAXException; - -import com.maxprograms.converters.Constants; -import com.maxprograms.converters.Convert; -import com.maxprograms.converters.EncodingResolver; -import com.maxprograms.converters.FileFormats; -import com.maxprograms.converters.Merge; -import com.maxprograms.converters.TmxExporter; -import com.maxprograms.languages.Language; -import com.maxprograms.languages.LanguageUtils; -import com.maxprograms.stats.RepetitionAnalysis; -import com.maxprograms.validation.XliffChecker; -import com.maxprograms.xliff2.ToXliff2; -import com.sun.net.httpserver.HttpExchange; -import com.sun.net.httpserver.HttpHandler; -import com.sun.net.httpserver.HttpServer; - -public class FilterServer implements HttpHandler { - - private static Logger LOGGER = System.getLogger(FilterServer.class.getName()); - - private HttpServer server; - private Hashtable running; - private Hashtable validationResults; - private boolean embed; - private String xliff; - private String catalog; - private boolean is20; - private String target; - private boolean unapproved; - private boolean exportTmx; - - public static void main(String[] args) { - String port = "8000"; - for (int i = 0; i < args.length; i++) { - String arg = args[i]; - if (arg.equals("-version")) { - LOGGER.log(Level.INFO, () -> "Version: " + Constants.VERSION + " Build: " + Constants.BUILD); - return; - } - if (arg.equals("-port") && (i + 1) < args.length) { - port = args[i + 1]; - } - } - try { - FilterServer instance = new FilterServer(Integer.valueOf(port)); - instance.run(); - } catch (Exception e) { - LOGGER.log(Level.ERROR, "Server error", e); - } - } - - public FilterServer(int port) throws IOException { - running = new Hashtable<>(); - validationResults = new Hashtable<>(); - server = HttpServer.create(new InetSocketAddress(port), 0); - server.createContext("/FilterServer", this); - server.setExecutor(null); // creates a default executor - } - - public void run() { - server.start(); - LOGGER.log(Level.INFO, "FilterServer started"); - } - - @Override - public void handle(HttpExchange t) throws IOException { - URI uri = t.getRequestURI(); - String request = ""; - try (InputStream is = t.getRequestBody()) { - request = readRequestBody(is); - } - - JSONObject json = null; - String response = ""; - String command = "version"; - try { - if (!request.isBlank()) { - json = new JSONObject(request); - command = json.getString("command"); - } - if (command.equals("version")) { - response = "{\"tool\":\"Open XLIFF Filters\", \"version\": \"" + Constants.VERSION + "\", \"build\": \"" - + Constants.BUILD + "\"}"; - } - if (command.equals("convert")) { - response = convert(json); - } - if (command.equals("merge")) { - response = merge(json); - } - if (command.equals("status")) { - response = getStatus(json); - } - if (command.equals("validationResult")) { - response = getValidationResult(json); - } - if (command.equals("getFileType")) { - response = getFileType(json); - } - if (command.equals("getTargetFile")) { - response = getTargetFile(json); - } - if (command.equals("validateXliff")) { - response = validateXliff(json); - } - if (command.equals("analyseXliff")) { - response = analyseXliff(json); - } - if (command.equals("getTypes") || uri.toString().endsWith("/getTypes")) { - response = getTypes(); - } - if (command.equals("getCharsets") || uri.toString().endsWith("/getCharsets")) { - response = getCharsets(); - } - if (command.equals("getLanguages") || uri.toString().endsWith("/getLanguages")) { - response = getLanguages(); - } - t.getResponseHeaders().add("content-type", "application/json"); - t.sendResponseHeaders(200, response.length()); - try (BufferedReader reader = new BufferedReader( - new InputStreamReader(new ByteArrayInputStream(response.getBytes())))) { - try (OutputStream os = t.getResponseBody()) { - String line; - while ((line = reader.readLine()) != null) { - os.write(line.getBytes()); - } - } - } - } catch (IOException | SAXException | ParserConfigurationException e) { - response = e.getMessage(); - t.sendResponseHeaders(500, response.length()); - try (OutputStream os = t.getResponseBody()) { - os.write(response.getBytes()); - } - } - } - - private String analyseXliff(JSONObject json) { - String file = json.getString("file"); - catalog = ""; - if (json.has("catalog")){ - catalog = json.getString("catalog"); - } - if (catalog.isEmpty()) { - File catalogFolder = new File(new File(System.getProperty("user.dir")), "catalog"); - catalog = new File(catalogFolder, "catalog.xml").getAbsolutePath(); - } - - String process = "" + System.currentTimeMillis(); - new Thread(new Runnable() { - - @Override - public void run() { - running.put(process, "running"); - try { - RepetitionAnalysis instance = new RepetitionAnalysis(); - instance.analyse(file, catalog); - if (running.get(process).equals(("running"))) { - LOGGER.log(Level.INFO, "Analysis completed"); - running.put(process, "completed"); - } - } catch (IOException | SAXException | ParserConfigurationException e) { - LOGGER.log(Level.ERROR, "Error analysing file", e); - running.put(process, e.getMessage()); - } - } - }).start(); - return "{\"process\":\"" + process + "\"}"; - } - - private String validateXliff(JSONObject json) { - String file = json.getString("file"); - catalog = ""; - if (json.has("catalog")) { - catalog = json.getString("catalog"); - } - if (catalog.isEmpty()) { - File catalogFolder = new File(new File(System.getProperty("user.dir")), "catalog"); - catalog = new File(catalogFolder, "catalog.xml").getAbsolutePath(); - } - - String process = "" + System.currentTimeMillis(); - new Thread(new Runnable() { - - @Override - public void run() { - running.put(process, "running"); - try { - XliffChecker validator = new XliffChecker(); - boolean valid = validator.validate(file, catalog); - JSONObject result = new JSONObject(); - result.put("valid", valid); - if (valid) { - String version = validator.getVersion(); - result.put("comment", "Selected file is valid XLIFF " + version); - } else { - String reason = validator.getReason(); - result.put("reason", reason); - } - validationResults.put(process, result); - if (running.get(process).equals(("running"))) { - LOGGER.log(Level.INFO, "Validation completed"); - running.put(process, "completed"); - } - } catch (IOException e) { - LOGGER.log(Level.ERROR, "Error validating file", e); - running.put(process, e.getMessage()); - } - } - }).start(); - return "{\"process\":\"" + process + "\"}"; - } - - private static String getTargetFile(JSONObject json) { - String file = json.getString("file"); - String target = ""; - try { - target = Merge.getTargetFile(file); - } catch (IOException | SAXException | ParserConfigurationException e) { - LOGGER.log(Level.ERROR, "Error getting target file", e); - } - if (target.isEmpty()) { - target = "Unknown"; - } - JSONObject result = new JSONObject(); - result.put("target", target); - return result.toString(); - } - - private static String getCharsets() { - JSONObject result = new JSONObject(); - JSONArray array = new JSONArray(); - result.put("charsets", array); - TreeMap charsets = new TreeMap<>(Charset.availableCharsets()); - Set keys = charsets.keySet(); - Iterator i = keys.iterator(); - while (i.hasNext()) { - Charset cset = charsets.get(i.next()); - JSONObject charset = new JSONObject(); - charset.put("code", cset.name()); - charset.put("description", cset.displayName()); - array.put(charset); - } - return result.toString(2); - } - - private String getStatus(JSONObject json) { - String status = "unknown"; - if (json.has("process")) { - String process = json.getString("process"); - status = running.get(process); - } - if (status == null) { - status = "Error"; - } - return "{\"status\": \"" + status + "\"}"; - } - - private String getValidationResult(JSONObject json) { - JSONObject result = new JSONObject(); - if (json.has("process")) { - String process = json.getString("process"); - result = validationResults.get(process); - validationResults.remove(process); - } else { - result.put("valid", false); - result.put("reason", "Error retrieving result from server"); - } - return result.toString(2); - } - - private String merge(JSONObject json) { - xliff = ""; - if (json.has("xliff")) { - xliff = json.getString("xliff"); - } - target = ""; - if (json.has("target")) { - target = json.getString("target"); - } - catalog = ""; - if (json.has("catalog")) { - catalog = json.getString("catalog"); - } - if (catalog.isEmpty()) { - File catalogFolder = new File(new File(System.getProperty("user.dir")), "catalog"); - catalog = new File(catalogFolder, "catalog.xml").getAbsolutePath(); - } - unapproved = false; - if (json.has("unapproved")) { - unapproved = json.getBoolean("unapproved"); - } - exportTmx = false; - if (json.has("exportTmx")) { - exportTmx = json.getBoolean("exportTmx"); - } - String process = "" + System.currentTimeMillis(); - new Thread(new Runnable() { - - @Override - public void run() { - running.put(process, "running"); - try { - Merge.merge(xliff, target, catalog, unapproved); - if (exportTmx) { - String tmx = ""; - if (xliff.toLowerCase().endsWith(".xlf")) { - tmx = xliff.substring(0, xliff.lastIndexOf('.')) + ".tmx"; - } else { - tmx = xliff + ".tmx"; - } - TmxExporter.export(xliff, tmx, catalog); - } - if (running.get(process).equals(("running"))) { - LOGGER.log(Level.INFO, "Merge completed"); - running.put(process, "completed"); - } - } catch (IOException | SAXException | ParserConfigurationException e) { - LOGGER.log(Level.ERROR, "Error merging file", e); - running.put(process, e.getMessage()); - } - } - }).start(); - return "{\"process\":\"" + process + "\"}"; - } - - private String convert(JSONObject json) { - String source = ""; - if (json.has("file")) { - source = json.getString("file"); - } - String srcLang = ""; - if (json.has("srcLang")) { - srcLang = json.getString("srcLang"); - } - String tgtLang = ""; - if (json.has("tgtLang")) { - tgtLang = json.getString("tgtLang"); - } - xliff = source + ".xlf"; - if (json.has("xliff")) { - xliff = json.getString("xliff"); - } - String skl = source + ".skl"; - if (json.has("sklFolder")) { - File sklFolder = new File(json.getString("sklFolder")); - if (!sklFolder.exists()) { - try { - Files.createDirectories(sklFolder.toPath()); - } catch (IOException e) { - LOGGER.log(Level.ERROR, "Unable to create skeleton folder " + json.getString("sklFolder")); - } - } - try { - File tmp = File.createTempFile(new File(source).getName(), ".skl", sklFolder); - skl = tmp.getAbsolutePath(); - } catch (IOException e) { - LOGGER.log(Level.ERROR, "Error creating skeleton", e); - } - } - if (json.has("skl")) { - skl = json.getString("skl"); - } - String type = ""; - if (json.has("type")) { - type = json.getString("type"); - String fullName = FileFormats.getFullName(type); - if (fullName != null) { - type = fullName; - } - } - if (type.isEmpty()) { - String detected = FileFormats.detectFormat(source); - if (detected != null) { - type = detected; - LOGGER.log(Level.INFO, "Auto-detected type: " + type); - } else { - LOGGER.log(Level.ERROR, "Unable to auto-detect file format. Use '-type' parameter."); - } - } - String enc = ""; - if (json.has("enc")) { - enc = json.getString("enc"); - } - if (enc.isEmpty()) { - Charset charset = EncodingResolver.getEncoding(source, type); - if (charset != null) { - enc = charset.name(); - LOGGER.log(Level.INFO, "Auto-detected encoding: " + enc); - } else { - LOGGER.log(Level.ERROR, "Unable to auto-detect character set. Use '-enc' parameter."); - } - } - String srx = ""; - if (json.has("srx")) { - srx = json.getString("srx"); - } - if (srx.isEmpty()) { - File srxFolder = new File(new File(System.getProperty("user.dir")), "srx"); - srx = new File(srxFolder, "default.srx").getAbsolutePath(); - } - catalog = ""; - if (json.has("catalog")) { - catalog = json.getString("catalog"); - } - if (catalog.isEmpty()) { - File catalogFolder = new File(new File(System.getProperty("user.dir")), "catalog"); - catalog = new File(catalogFolder, "catalog.xml").getAbsolutePath(); - } - String ditaval = ""; - if (json.has("ditaval") ) { - ditaval = json.getString("ditaval"); - } - embed = false; - if (json.has("embed")) { - embed = json.getBoolean("embed"); - } - boolean paragraph = false; - if (json.has("paragraph")) { - paragraph = json.getBoolean("paragraph"); - } - is20 = false; - if (json.has("is20")) { - is20 = json.getBoolean("is20"); - } - String process = "" + System.currentTimeMillis(); - - Hashtable params = new Hashtable<>(); - params.put("source", source); - params.put("srcLang", srcLang); - params.put("xliff", xliff); - params.put("skeleton", skl); - params.put("format", type); - params.put("catalog", catalog); - params.put("srcEncoding", enc); - params.put("paragraph", paragraph ? "yes" : "no"); - params.put("srxFile", srx); - if (!tgtLang.isEmpty()) { - params.put("tgtLang", tgtLang); - } - if (type.equals(FileFormats.DITA) && !ditaval.isEmpty()) { - params.put("ditaval", ditaval); - } - - new Thread(new Runnable() { - - @Override - public void run() { - running.put(process, "running"); - Vector result = Convert.run(params); - if ("0".equals(result.get(0))) { - if (embed) { - try { - Convert.addSkeleton(xliff, catalog); - } catch (SAXException | IOException | ParserConfigurationException e) { - LOGGER.log(Level.ERROR, "Error embedding skeleton", e); - running.put(process, e.getMessage()); - is20 = false; - } - } - if (is20) { - result = ToXliff2.run(new File(xliff), catalog); - if (!"0".equals(result.get(0))) { - LOGGER.log(Level.ERROR, result.get(1)); - running.put(process, result.get(1)); - } - } - } else { - LOGGER.log(Level.ERROR, result.get(1)); - running.put(process, result.get(1)); - } - if (running.get(process).equals(("running"))) { - LOGGER.log(Level.INFO, "Conversion completed"); - running.put(process, "completed"); - } - } - }).start(); - return "{\"process\":\"" + process + "\"}"; - } - - private static String readRequestBody(InputStream is) throws IOException { - StringBuilder request = new StringBuilder(); - try (BufferedReader rd = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { - String line; - while ((line = rd.readLine()) != null) { - request.append(line); - } - } - return request.toString(); - } - - private static String getFileType(JSONObject json) { - String file = json.getString("file"); - String type = "Unknown"; - String encoding = "Unknown"; - String detected = FileFormats.detectFormat(file); - if (detected != null) { - type = FileFormats.getShortName(detected); - if (type != null) { - Charset charset = EncodingResolver.getEncoding(file, detected); - if (charset != null) { - encoding = charset.name(); - } - } - } - if (encoding.equals("Unknown")) { - try { - Charset bom = EncodingResolver.getBOM(file); - if (bom != null) { - encoding = bom.name(); - } - } catch (IOException e) { - // ignore - } - } - JSONObject result = new JSONObject(); - result.put("file", file); - result.put("type", type); - result.put("encoding", encoding); - return result.toString(); - } - - private static String getTypes() { - String[] formats = FileFormats.getFormats(); - StringBuilder builder = new StringBuilder(); - builder.append("{\"types\": [\n"); - for (int i = 0; i < formats.length; i++) { - if (i > 0) { - builder.append(",\n"); - } - builder.append("{\"type\":\""); - builder.append(FileFormats.getShortName(formats[i])); - builder.append("\", \"description\":\""); - builder.append(formats[i]); - builder.append("\"}"); - } - builder.append("]}\n"); - return builder.toString(); - } - - private static String getLanguages() throws SAXException, IOException, ParserConfigurationException { - List languages = LanguageUtils.getCommonLanguages(); - StringBuilder builder = new StringBuilder(); - builder.append("{\"languages\": [\n"); - for (int i = 0; i < languages.size(); i++) { - Language lang = languages.get(i); - if (i > 0) { - builder.append(",\n"); - } - builder.append("{\"code\":\""); - builder.append(lang.getCode()); - builder.append("\", \"description\":\""); - builder.append(lang.getDescription()); - builder.append("\"}"); - } - builder.append("]}\n"); - return builder.toString(); - } - -} +/******************************************************************************* + * Copyright (c) 2003-2019 Maxprograms. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/org/documents/epl-v10.html + * + * Contributors: + * Maxprograms - initial API and implementation + *******************************************************************************/ + +package com.maxprograms.server; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.lang.System.Logger; +import java.lang.System.Logger.Level; +import java.net.InetSocketAddress; +import java.net.URI; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.TreeMap; +import java.util.Vector; + +import javax.xml.parsers.ParserConfigurationException; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.xml.sax.SAXException; + +import com.maxprograms.converters.Constants; +import com.maxprograms.converters.Convert; +import com.maxprograms.converters.EncodingResolver; +import com.maxprograms.converters.FileFormats; +import com.maxprograms.converters.Merge; +import com.maxprograms.converters.TmxExporter; +import com.maxprograms.languages.Language; +import com.maxprograms.languages.LanguageUtils; +import com.maxprograms.stats.RepetitionAnalysis; +import com.maxprograms.validation.XliffChecker; +import com.maxprograms.xliff2.ToXliff2; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; + +public class FilterServer implements HttpHandler { + + private static Logger LOGGER = System.getLogger(FilterServer.class.getName()); + + private HttpServer server; + private Hashtable running; + private Hashtable validationResults; + private Hashtable conversionResults; + private Hashtable mergeResults; + private Hashtable analysisResults; + private boolean embed; + private String xliff; + private String catalog; + private boolean is20; + private String target; + private boolean unapproved; + private boolean exportTmx; + + public static void main(String[] args) { + String port = "8000"; + for (int i = 0; i < args.length; i++) { + String arg = args[i]; + if (arg.equals("-version")) { + LOGGER.log(Level.INFO, () -> "Version: " + Constants.VERSION + " Build: " + Constants.BUILD); + return; + } + if (arg.equals("-port") && (i + 1) < args.length) { + port = args[i + 1]; + } + } + try { + FilterServer instance = new FilterServer(Integer.valueOf(port)); + instance.run(); + } catch (Exception e) { + LOGGER.log(Level.ERROR, "Server error", e); + } + } + + public FilterServer(int port) throws IOException { + running = new Hashtable<>(); + validationResults = new Hashtable<>(); + conversionResults = new Hashtable<>(); + mergeResults = new Hashtable<>(); + analysisResults = new Hashtable<>(); + server = HttpServer.create(new InetSocketAddress(port), 0); + server.createContext("/FilterServer", this); + server.setExecutor(null); // creates a default executor + } + + public void run() { + server.start(); + LOGGER.log(Level.INFO, "FilterServer started"); + } + + @Override + public void handle(HttpExchange t) throws IOException { + URI uri = t.getRequestURI(); + String request = ""; + try (InputStream is = t.getRequestBody()) { + request = readRequestBody(is); + } + + JSONObject json = null; + String response = ""; + String command = "version"; + try { + if (uri.toString().endsWith("/stop")) { + command = "stop"; + response = "{\"stopping\": \"now!\"}"; + } + if (!request.isBlank()) { + json = new JSONObject(request); + command = json.getString("command"); + } + if (command.equals("version")) { + response = "{\"tool\":\"" + Constants.TOOLNAME + "\", \"version\": \"" + Constants.VERSION + "\", \"build\": \"" + + Constants.BUILD + "\"}"; + } + if (command.equals("convert")) { + response = convert(json); + } + if (command.equals("merge")) { + response = merge(json); + } + if (command.equals("status")) { + response = getStatus(json); + } + if (command.equals("validationResult")) { + response = getValidationResult(json); + } + if (command.equals("conversionResult")) { + response = getConversionResult(json); + } + if (command.equals("mergeResult")) { + response = getMergeResult(json); + } + if (command.equals("analysisResult")) { + response = getAnalysisResult(json); + } + if (command.equals("getFileType")) { + response = getFileType(json); + } + if (command.equals("getTargetFile")) { + response = getTargetFile(json); + } + if (command.equals("validateXliff")) { + response = validateXliff(json); + } + if (command.equals("analyseXliff")) { + response = analyseXliff(json); + } + if (command.equals("getTypes") || uri.toString().endsWith("/getTypes")) { + response = getTypes(); + } + if (command.equals("getCharsets") || uri.toString().endsWith("/getCharsets")) { + response = getCharsets(); + } + if (command.equals("getLanguages") || uri.toString().endsWith("/getLanguages")) { + response = getLanguages(); + } + t.getResponseHeaders().add("content-type", "application/json"); + t.sendResponseHeaders(200, response.length()); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(new ByteArrayInputStream(response.getBytes())))) { + try (OutputStream os = t.getResponseBody()) { + String line; + while ((line = reader.readLine()) != null) { + os.write(line.getBytes()); + } + } + } + if (command.equals("stop")) { + server.removeContext("/FilterServer"); + LOGGER.log(Level.INFO, "Server closed"); + System.exit(0); + } + } catch (IOException | SAXException | ParserConfigurationException e) { + response = e.getMessage(); + t.sendResponseHeaders(500, response.length()); + try (OutputStream os = t.getResponseBody()) { + os.write(response.getBytes()); + } + } + } + + private String getAnalysisResult(JSONObject json) { + JSONObject result = new JSONObject(); + if (json.has("process")) { + String process = json.getString("process"); + result = analysisResults.get(process); + analysisResults.remove(process); + } else { + result.put("result", "Failed"); + result.put("reason", "Error retrieving result from server"); + } + return result.toString(2); + } + + private String getMergeResult(JSONObject json) { + JSONObject result = new JSONObject(); + if (json.has("process")) { + String process = json.getString("process"); + result = mergeResults.get(process); + mergeResults.remove(process); + } else { + result.put("result", "Failed"); + result.put("reason", "Error retrieving result from server"); + } + return result.toString(2); + } + + private String getConversionResult(JSONObject json) { + JSONObject result = new JSONObject(); + if (json.has("process")) { + String process = json.getString("process"); + result = conversionResults.get(process); + conversionResults.remove(process); + } else { + result.put("result", "Failed"); + result.put("reason", "Error retrieving result from server"); + } + return result.toString(2); + } + + private String analyseXliff(JSONObject json) { + String file = json.getString("file"); + catalog = ""; + if (json.has("catalog")){ + catalog = json.getString("catalog"); + } + if (catalog.isEmpty()) { + File catalogFolder = new File(new File(System.getProperty("user.dir")), "catalog"); + catalog = new File(catalogFolder, "catalog.xml").getAbsolutePath(); + } + + String process = "" + System.currentTimeMillis(); + new Thread(new Runnable() { + + @Override + public void run() { + running.put(process, "running"); + Vector result = new Vector<>(); + try { + RepetitionAnalysis instance = new RepetitionAnalysis(); + instance.analyse(file, catalog); + result.add(Constants.SUCCESS); + } catch (IOException | SAXException | ParserConfigurationException e) { + LOGGER.log(Level.ERROR, "Error analysing file", e); + result.add(Constants.ERROR); + result.add(e.getMessage()); + } + JSONObject jsonResult = new JSONObject(); + if (Constants.SUCCESS.equals(result.get(0))) { + jsonResult.put("result", "Success"); + } else { + jsonResult.put("result", "Failed"); + jsonResult.put("reason", result.get(1)); + } + analysisResults.put(process, jsonResult); + running.put(process, "completed"); + } + }).start(); + return "{\"process\":\"" + process + "\"}"; + } + + private String validateXliff(JSONObject json) { + String file = json.getString("file"); + catalog = ""; + if (json.has("catalog")) { + catalog = json.getString("catalog"); + } + if (catalog.isEmpty()) { + File catalogFolder = new File(new File(System.getProperty("user.dir")), "catalog"); + catalog = new File(catalogFolder, "catalog.xml").getAbsolutePath(); + } + + String process = "" + System.currentTimeMillis(); + new Thread(new Runnable() { + + @Override + public void run() { + running.put(process, "running"); + try { + XliffChecker validator = new XliffChecker(); + boolean valid = validator.validate(file, catalog); + JSONObject result = new JSONObject(); + result.put("valid", valid); + if (valid) { + String version = validator.getVersion(); + result.put("comment", "Selected file is valid XLIFF " + version); + } else { + String reason = validator.getReason(); + result.put("reason", reason); + } + validationResults.put(process, result); + if (running.get(process).equals(("running"))) { + LOGGER.log(Level.INFO, "Validation completed"); + running.put(process, "completed"); + } + } catch (IOException e) { + LOGGER.log(Level.ERROR, "Error validating file", e); + running.put(process, e.getMessage()); + } + } + }).start(); + return "{\"process\":\"" + process + "\"}"; + } + + private static String getTargetFile(JSONObject json) { + JSONObject result = new JSONObject(); + String file = json.getString("file"); + String target = ""; + try { + target = Merge.getTargetFile(file); + if (target.isEmpty()) { + target = "Unknown"; + } + result.put("result", "Success"); + } catch (IOException | SAXException | ParserConfigurationException e) { + LOGGER.log(Level.ERROR, "Error getting target file", e); + result.put("result", "Failed"); + result.put("reason", e.getMessage()); + } + result.put("target", target); + return result.toString(); + } + + private static String getCharsets() { + JSONObject result = new JSONObject(); + JSONArray array = new JSONArray(); + result.put("charsets", array); + TreeMap charsets = new TreeMap<>(Charset.availableCharsets()); + Set keys = charsets.keySet(); + Iterator i = keys.iterator(); + while (i.hasNext()) { + Charset cset = charsets.get(i.next()); + JSONObject charset = new JSONObject(); + charset.put("code", cset.name()); + charset.put("description", cset.displayName()); + array.put(charset); + } + return result.toString(2); + } + + private String getStatus(JSONObject json) { + String status = "unknown"; + if (json.has("process")) { + String process = json.getString("process"); + status = running.get(process); + } + if (status == null) { + status = "Error"; + } + return "{\"status\": \"" + status + "\"}"; + } + + private String getValidationResult(JSONObject json) { + JSONObject result = new JSONObject(); + if (json.has("process")) { + String process = json.getString("process"); + result = validationResults.get(process); + validationResults.remove(process); + } else { + result.put("valid", false); + result.put("reason", "Error retrieving result from server"); + } + return result.toString(2); + } + + private String merge(JSONObject json) { + xliff = ""; + if (json.has("xliff")) { + xliff = json.getString("xliff"); + } + target = ""; + if (json.has("target")) { + target = json.getString("target"); + } + catalog = ""; + if (json.has("catalog")) { + catalog = json.getString("catalog"); + } + if (catalog.isEmpty()) { + File catalogFolder = new File(new File(System.getProperty("user.dir")), "catalog"); + catalog = new File(catalogFolder, "catalog.xml").getAbsolutePath(); + } + unapproved = false; + if (json.has("unapproved")) { + unapproved = json.getBoolean("unapproved"); + } + exportTmx = false; + if (json.has("exportTmx")) { + exportTmx = json.getBoolean("exportTmx"); + } + String process = "" + System.currentTimeMillis(); + new Thread(new Runnable() { + + @Override + public void run() { + running.put(process, "running"); + + Vector result = Merge.merge(xliff, target, catalog, unapproved); + if (exportTmx && Constants.SUCCESS.equals(result.get(0))) { + String tmx = ""; + if (xliff.toLowerCase().endsWith(".xlf")) { + tmx = xliff.substring(0, xliff.lastIndexOf('.')) + ".tmx"; + } else { + tmx = xliff + ".tmx"; + } + result = TmxExporter.export(xliff, tmx, catalog); + } + JSONObject jsonResult = new JSONObject(); + if (Constants.SUCCESS.equals(result.get(0))) { + jsonResult.put("result", "Success"); + } else { + jsonResult.put("result", "Failed"); + jsonResult.put("reason", result.get(1)); + } + mergeResults.put(process, jsonResult); + if (running.get(process).equals(("running"))) { + running.put(process, "completed"); + } + } + }).start(); + return "{\"process\":\"" + process + "\"}"; + } + + private String convert(JSONObject json) { + String source = ""; + if (json.has("file")) { + source = json.getString("file"); + } + String srcLang = ""; + if (json.has("srcLang")) { + srcLang = json.getString("srcLang"); + } + String tgtLang = ""; + if (json.has("tgtLang")) { + tgtLang = json.getString("tgtLang"); + } + xliff = source + ".xlf"; + if (json.has("xliff")) { + xliff = json.getString("xliff"); + } + String skl = source + ".skl"; + if (json.has("sklFolder")) { + File sklFolder = new File(json.getString("sklFolder")); + if (!sklFolder.exists()) { + try { + Files.createDirectories(sklFolder.toPath()); + } catch (IOException e) { + LOGGER.log(Level.ERROR, "Unable to create skeleton folder " + json.getString("sklFolder")); + } + } + try { + File tmp = File.createTempFile(new File(source).getName(), ".skl", sklFolder); + skl = tmp.getAbsolutePath(); + } catch (IOException e) { + LOGGER.log(Level.ERROR, "Error creating skeleton", e); + } + } + if (json.has("skl")) { + skl = json.getString("skl"); + } + String type = ""; + if (json.has("type")) { + type = json.getString("type"); + String fullName = FileFormats.getFullName(type); + if (fullName != null) { + type = fullName; + } + } + if (type.isEmpty()) { + String detected = FileFormats.detectFormat(source); + if (detected != null) { + type = detected; + LOGGER.log(Level.INFO, "Auto-detected type: " + type); + } else { + LOGGER.log(Level.ERROR, "Unable to auto-detect file format. Use '-type' parameter."); + } + } + String enc = ""; + if (json.has("enc")) { + enc = json.getString("enc"); + } + if (enc.isEmpty()) { + Charset charset = EncodingResolver.getEncoding(source, type); + if (charset != null) { + enc = charset.name(); + LOGGER.log(Level.INFO, "Auto-detected encoding: " + enc); + } else { + LOGGER.log(Level.ERROR, "Unable to auto-detect character set. Use '-enc' parameter."); + } + } + String srx = ""; + if (json.has("srx")) { + srx = json.getString("srx"); + } + if (srx.isEmpty()) { + File srxFolder = new File(new File(System.getProperty("user.dir")), "srx"); + srx = new File(srxFolder, "default.srx").getAbsolutePath(); + } + catalog = ""; + if (json.has("catalog")) { + catalog = json.getString("catalog"); + } + if (catalog.isEmpty()) { + File catalogFolder = new File(new File(System.getProperty("user.dir")), "catalog"); + catalog = new File(catalogFolder, "catalog.xml").getAbsolutePath(); + } + String ditaval = ""; + if (json.has("ditaval") ) { + ditaval = json.getString("ditaval"); + } + embed = false; + if (json.has("embed")) { + embed = json.getBoolean("embed"); + } + boolean paragraph = false; + if (json.has("paragraph")) { + paragraph = json.getBoolean("paragraph"); + } + is20 = false; + if (json.has("is20")) { + is20 = json.getBoolean("is20"); + } + String process = "" + System.currentTimeMillis(); + + Hashtable params = new Hashtable<>(); + params.put("source", source); + params.put("srcLang", srcLang); + params.put("xliff", xliff); + params.put("skeleton", skl); + params.put("format", type); + params.put("catalog", catalog); + params.put("srcEncoding", enc); + params.put("paragraph", paragraph ? "yes" : "no"); + params.put("srxFile", srx); + if (!tgtLang.isEmpty()) { + params.put("tgtLang", tgtLang); + } + if (type.equals(FileFormats.DITA) && !ditaval.isEmpty()) { + params.put("ditaval", ditaval); + } + + new Thread(new Runnable() { + + @Override + public void run() { + running.put(process, "running"); + Vector result = Convert.run(params); + if (embed && Constants.SUCCESS.equals(result.get(0))) { + result = Convert.addSkeleton(xliff, catalog); + } + if (is20 && Constants.SUCCESS.equals(result.get(0))) { + result = ToXliff2.run(new File(xliff), catalog); + } + JSONObject jsonResult = new JSONObject(); + if (Constants.SUCCESS.equals(result.get(0))) { + jsonResult.put("result", "Success"); + } else { + jsonResult.put("result", "Failed"); + jsonResult.put("reason", result.get(1)); + } + conversionResults.put(process, jsonResult); + if (running.get(process).equals(("running"))) { + running.put(process, "completed"); + } + } + }).start(); + return "{\"process\":\"" + process + "\"}"; + } + + private static String readRequestBody(InputStream is) throws IOException { + StringBuilder request = new StringBuilder(); + try (BufferedReader rd = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + String line; + while ((line = rd.readLine()) != null) { + request.append(line); + } + } + return request.toString(); + } + + private static String getFileType(JSONObject json) { + String file = json.getString("file"); + String type = "Unknown"; + String encoding = "Unknown"; + String detected = FileFormats.detectFormat(file); + if (detected != null) { + type = FileFormats.getShortName(detected); + if (type != null) { + Charset charset = EncodingResolver.getEncoding(file, detected); + if (charset != null) { + encoding = charset.name(); + } + } + } + if (encoding.equals("Unknown")) { + try { + Charset bom = EncodingResolver.getBOM(file); + if (bom != null) { + encoding = bom.name(); + } + } catch (IOException e) { + // ignore + } + } + JSONObject result = new JSONObject(); + result.put("file", file); + result.put("type", type); + result.put("encoding", encoding); + return result.toString(); + } + + private static String getTypes() { + String[] formats = FileFormats.getFormats(); + StringBuilder builder = new StringBuilder(); + builder.append("{\"types\": [\n"); + for (int i = 0; i < formats.length; i++) { + if (i > 0) { + builder.append(",\n"); + } + builder.append("{\"type\":\""); + builder.append(FileFormats.getShortName(formats[i])); + builder.append("\", \"description\":\""); + builder.append(formats[i]); + builder.append("\"}"); + } + builder.append("]}\n"); + return builder.toString(); + } + + private static String getLanguages() throws SAXException, IOException, ParserConfigurationException { + List languages = LanguageUtils.getCommonLanguages(); + StringBuilder builder = new StringBuilder(); + builder.append("{\"languages\": [\n"); + for (int i = 0; i < languages.size(); i++) { + Language lang = languages.get(i); + if (i > 0) { + builder.append(",\n"); + } + builder.append("{\"code\":\""); + builder.append(lang.getCode()); + builder.append("\", \"description\":\""); + builder.append(lang.getDescription()); + builder.append("\"}"); + } + builder.append("]}\n"); + return builder.toString(); + } + +} diff --git a/src/com/maxprograms/xliff2/FromXliff2.java b/src/com/maxprograms/xliff2/FromXliff2.java index f76a1a57..423be367 100644 --- a/src/com/maxprograms/xliff2/FromXliff2.java +++ b/src/com/maxprograms/xliff2/FromXliff2.java @@ -25,6 +25,7 @@ import org.xml.sax.SAXException; +import com.maxprograms.converters.Constants; import com.maxprograms.xml.Catalog; import com.maxprograms.xml.Document; import com.maxprograms.xml.Element; @@ -53,7 +54,7 @@ public static Vector run(String sourceFile, String outputFile, String ca Document doc = builder.build(sourceFile); Element root = doc.getRootElement(); if (!root.getAttributeValue("version", "2.0").equals("2.0")) { - result.add("1"); + result.add(Constants.ERROR); result.add("Wrong XLIFF version."); return result; } @@ -67,11 +68,11 @@ public static Vector run(String sourceFile, String outputFile, String ca out.write(XMLUtils.UTF8BOM); outputter.output(xliff12, out); } - result.addElement("0"); + result.add(Constants.SUCCESS); } catch (SAXException | IOException | ParserConfigurationException ex) { Logger logger = System.getLogger(FromXliff2.class.getName()); logger.log(Level.ERROR, "Error processing XLIFF 2.0", ex); - result.add("1"); + result.add(Constants.ERROR); result.add(ex.getMessage()); } return result; diff --git a/src/com/maxprograms/xliff2/ToXliff2.java b/src/com/maxprograms/xliff2/ToXliff2.java index 3e7e3439..eb9ace8a 100644 --- a/src/com/maxprograms/xliff2/ToXliff2.java +++ b/src/com/maxprograms/xliff2/ToXliff2.java @@ -27,6 +27,7 @@ import org.xml.sax.SAXException; +import com.maxprograms.converters.Constants; import com.maxprograms.xml.Catalog; import com.maxprograms.xml.Document; import com.maxprograms.xml.Element; @@ -60,7 +61,7 @@ public static Vector run(String sourceFile, String outputFile, String ca Document doc = builder.build(sourceFile); Element root = doc.getRootElement(); if (!root.getAttributeValue("version", "1.2").equals("1.2")) { - result.add("1"); + result.add(Constants.ERROR); result.add("Wrong XLIFF version."); return result; } @@ -74,11 +75,11 @@ public static Vector run(String sourceFile, String outputFile, String ca out.write(XMLUtils.UTF8BOM); outputter.output(xliff2, out); } - result.addElement("0"); + result.add(Constants.SUCCESS); } catch (SAXException | IOException | ParserConfigurationException ex) { Logger logger = System.getLogger(ToXliff2.class.getName()); logger.log(Level.ERROR, "Error generating XLIFF 2.0"); - result.add("1"); + result.add(Constants.ERROR); result.add(ex.getMessage()); } return result;