rspolib-0.1.1/.cargo_vcs_info.json0000644000000001420000000000100125020ustar { "git": { "sha1": "4da9f25405e016a9efd65a4a370e0a740851b9c4" }, "path_in_vcs": "rust" }rspolib-0.1.1/.gitignore000064400000000000000000000000361046102023000132640ustar 00000000000000/*.po /*.mo coverage*.profraw rspolib-0.1.1/Cargo.lock0000644000000426000000000000100104620ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "anes" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstyle" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "bumpalo" version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "cast" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "ciborium" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" dependencies = [ "ciborium-io", "ciborium-ll", "serde", ] [[package]] name = "ciborium-io" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" [[package]] name = "ciborium-ll" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" dependencies = [ "ciborium-io", "half", ] [[package]] name = "clap" version = "4.5.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8eb5e908ef3a6efbe1ed62520fb7287959888c88485abe072543190ecc66783" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" version = "4.5.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96b01801b5fc6a0a232407abc821660c9c6d25a1cafc0d4f85f29fb8d9afc121" dependencies = [ "anstyle", "clap_lex", ] [[package]] name = "clap_lex" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "criterion" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" dependencies = [ "anes", "cast", "ciborium", "clap", "criterion-plot", "is-terminal", "itertools", "num-traits", "once_cell", "oorandom", "plotters", "rayon", "regex", "serde", "serde_derive", "serde_json", "tinytemplate", "walkdir", ] [[package]] name = "criterion-plot" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" dependencies = [ "cast", "itertools", ] [[package]] name = "crossbeam-deque" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "either" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "half" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" dependencies = [ "cfg-if", "crunchy", ] [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" [[package]] name = "is-terminal" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" dependencies = [ "hermit-abi", "libc", "windows-sys 0.52.0", ] [[package]] name = "itertools" version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ "either", ] [[package]] name = "itoa" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "js-sys" version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ "once_cell", "wasm-bindgen", ] [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "log" version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "natord" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c" [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "once_cell" version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "oorandom" version = "11.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" [[package]] name = "plotters" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" dependencies = [ "num-traits", "plotters-backend", "plotters-svg", "wasm-bindgen", "web-sys", ] [[package]] name = "plotters-backend" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" [[package]] name = "plotters-svg" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" dependencies = [ "plotters-backend", ] [[package]] name = "proc-macro2" version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] [[package]] name = "rayon" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", ] [[package]] name = "rayon-core" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" dependencies = [ "crossbeam-deque", "crossbeam-utils", ] [[package]] name = "regex" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rspolib" version = "0.1.1" dependencies = [ "criterion", "lazy_static", "natord", "snafu", "unicode-linebreak", "unicode-width", ] [[package]] name = "rustversion" version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" [[package]] name = "ryu" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "serde" version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b" dependencies = [ "itoa", "memchr", "ryu", "serde", ] [[package]] name = "snafu" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "223891c85e2a29c3fe8fb900c1fae5e69c2e42415e3177752e8718475efa5019" dependencies = [ "snafu-derive", ] [[package]] name = "snafu-derive" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03c3c6b7927ffe7ecaa769ee0e3994da3b8cafc8f444578982c83ecb161af917" dependencies = [ "heck", "proc-macro2", "quote", "syn", ] [[package]] name = "syn" version = "2.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tinytemplate" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" dependencies = [ "serde", "serde_json", ] [[package]] name = "unicode-ident" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "unicode-linebreak" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-width" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "wasm-bindgen" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "winapi-util" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets", ] [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" rspolib-0.1.1/Cargo.toml0000644000000024250000000000100105060ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.66.1" name = "rspolib" version = "0.1.1" build = "build.rs" autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "PO and MO files manipulation library." documentation = "https://docs.rs/rspolib" readme = "README.md" license = "MIT" repository = "https://github.com/mondeja/rspolib" [lib] name = "rspolib" path = "src/lib.rs" [[bench]] name = "formatting" path = "benches/formatting.rs" harness = false [[bench]] name = "parsing" path = "benches/parsing.rs" harness = false [dependencies.lazy_static] version = "1" [dependencies.natord] version = "1" [dependencies.snafu] version = "0.8" [dependencies.unicode-linebreak] version = "0.1" [dependencies.unicode-width] version = ">0.1" [dev-dependencies.criterion] version = "0.5" rspolib-0.1.1/Cargo.toml.orig000064400000000000000000000011241046102023000141620ustar 00000000000000[package] name = "rspolib" version = "0.1.1" rust-version = "1.66.1" edition = "2021" readme = "../README.md" description = "PO and MO files manipulation library." license = "MIT" documentation = "https://docs.rs/rspolib" repository = "https://github.com/mondeja/rspolib" [lib] name = "rspolib" path = "src/lib.rs" [dependencies] unicode-linebreak = "0.1" unicode-width = ">0.1" natord = "1" snafu = "0.8" lazy_static = "1" [dev-dependencies] rspolib-testing = { path = "./testing" } criterion = "0.5" [[bench]] name = "parsing" harness = false [[bench]] name = "formatting" harness = false rspolib-0.1.1/README.md000064400000000000000000000025151046102023000125570ustar 00000000000000# rspolib [![crates.io](https://img.shields.io/crates/v/rspolib?logo=rust)](https://crates.io/crates/rspolib) [![PyPI](https://img.shields.io/pypi/v/rspolib?logo=pypi&logoColor=white)](https://pypi.org/project/rspolib) [![docs.rs](https://img.shields.io/docsrs/rspolib?logo=docs.rs)](https://docs.rs/rspolib) [![Bindings docs](https://img.shields.io/badge/bindings-docs-blue?logo=python&logoColor=white)](https://github.com/mondeja/rspolib/blob/master/python/REFERENCE.md) Port to Rust of the Python library [polib]. ## Install ```bash cargo add rspolib ``` ## Usage ```rust use rspolib::{pofile, prelude::*}; let po = pofile("./tests-data/flags.po").unwrap(); for entry in &po.entries { println!("{}", entry.msgid); } po.save("./file.po"); ``` See the documentation at [docs.rs/rspolib](https://docs.rs/rspolib) ## Python bindings [![Python versions](https://img.shields.io/pypi/pyversions/rspolib?logo=python&logoColor=white)](https://pypi.org/project/rspolib/#files) - [Quickstart](https://github.com/mondeja/rspolib/tree/master/python#readme) - [Reference](https://github.com/mondeja/rspolib/blob/master/python/REFERENCE.md) ### Usage ```python import polib import rspolib rspo = rspolib.pofile(f"{tests_dir}/django-complete.po") pypo = polib.pofile(f"{tests_dir}/django-complete.po") ``` [polib]: https://github.com/izimobil/polib rspolib-0.1.1/benches/formatting.rs000064400000000000000000000015331046102023000154260ustar 00000000000000use criterion::{ black_box, criterion_group, criterion_main, Criterion, }; use rspolib::{mofile, pofile, MOFile, POFile}; fn pofile_to_string(file: &POFile) { file.to_string(); } fn mofile_to_string(file: &MOFile) { file.to_string(); } fn criterion_benchmark(c: &mut Criterion) { c.bench_function( "POFile('django-complete.po').to_string()", |b| { b.iter(|| { pofile_to_string(black_box( &pofile("tests-data/django-complete.po").unwrap(), )) }) }, ); c.bench_function("MOFile('all.mo').to_string()", |b| { b.iter(|| { mofile_to_string(black_box( &mofile("tests-data/all.mo").unwrap(), )) }) }); } criterion_group!(benches, criterion_benchmark); criterion_main!(benches); rspolib-0.1.1/benches/parsing.rs000064400000000000000000000012361046102023000147170ustar 00000000000000use criterion::{ black_box, criterion_group, criterion_main, Criterion, }; use rspolib::{mofile, pofile}; fn pofile_parse(basename: &str) { pofile(format!("tests-data/{}", basename).as_str()).ok(); } fn mofile_parse(basename: &str) { mofile(format!("tests-data/{}", basename).as_str()).ok(); } fn criterion_benchmark(c: &mut Criterion) { c.bench_function("pofile('django-complete.po')", |b| { b.iter(|| pofile_parse(black_box("django-complete.po"))) }); c.bench_function("mofile('all.mo')", |b| { b.iter(|| mofile_parse(black_box("all.mo"))) }); } criterion_group!(benches, criterion_benchmark); criterion_main!(benches); rspolib-0.1.1/build.rs000064400000000000000000000050221046102023000127410ustar 00000000000000// build.rs use std::env; use std::fs; use std::path::Path; fn generate_transitions( symbol: &str, states: &[&str], next_state: &str, ) -> String { let transitions = states .iter() .map(|state| { format!( " ((St::{symbol}, St::{state}), (St::{next_state}, St::{next_state}))," ) }) .collect::>() .join(""); transitions } macro_rules! add { ($r:ident, $symbol:literal, $states:expr, $next_state:literal) => { $r.push_str(&generate_transitions( $symbol, $states, $next_state, )); }; } macro_rules! transitions_table { ($r:ident) => { let all = &[ "ST", "HE", "GC", "OC", "FL", "CT", "PC", "PM", "PP", "TC", "MS", "MP", "MX", "MI", ]; add!($r, "TC", &["ST", "HE"], "HE"); add!( $r, "TC", &[ "GC", "OC", "FL", "TC", "PC", "PM", "PP", "MS", "MP", "MX", "MI" ], "TC" ); add!($r, "GC", all, "GC"); add!($r, "OC", all, "OC"); add!($r, "FL", all, "FL"); add!($r, "PC", all, "PC"); add!($r, "PM", all, "PM"); add!($r, "PP", all, "PP"); add!( $r, "CT", &[ "ST", "HE", "GC", "OC", "FL", "TC", "PC", "PM", "PP", "MS", "MX" ], "CT" ); add!( $r, "MI", &[ "ST", "HE", "GC", "OC", "FL", "CT", "TC", "PC", "PM", "PP", "MS", "MX" ], "MI" ); add!($r, "MP", &["TC", "GC", "PC", "PM", "PP", "MI"], "MP"); add!($r, "MS", &["MI", "MP", "TC"], "MS"); add!($r, "MX", &["MI", "MX", "MP", "TC"], "MX"); add!( $r, "MC", &["CT", "MI", "MP", "MS", "MX", "PM", "PP", "PC"], "MC" ); }; } fn generate_build_transitions_function() -> String { let mut r = String::from( "fn build_transitions() -> Transitions { HashMap::from([", ); transitions_table!(r); r.push_str( " ]) }", ); r } fn main() { let out_dir = env::var_os("OUT_DIR").unwrap(); let dest_path = Path::new(&out_dir).join("poparser-transitions.rs"); fs::write(dest_path, generate_build_transitions_function()) .unwrap(); println!("cargo:rerun-if-changed=build.rs"); } rspolib-0.1.1/src/entry/mod.rs000064400000000000000000000245371046102023000143650ustar 00000000000000use std::borrow::Cow; use std::fmt; use unicode_width::UnicodeWidthStr; use crate::escaping::escape; use crate::twrapper::wrap; pub mod moentry; pub mod poentry; pub use moentry::MOEntry; pub use poentry::POEntry; /// Provides a function `translated` to represent /// if an entry struct is translated pub trait Translated { fn translated(&self) -> bool; } /// Concatenates `msgid` + `EOT` + `msgctxt` /// /// The MO files spec indicates: /// /// > Contexts are stored (in MO files) by storing /// > the concatenation of the context, a EOT byte, /// > and the original string. /// /// This trait provides a way to get the string /// representation of `msgid` + `EOT` + `msgctxt`. /// /// Function required to generate MO files as /// the returned value is used as key on the /// translations table. pub trait MsgidEotMsgctxt { /// Returns `msgid` + (optionally: `EOT` + `msgctxt`) fn msgid_eot_msgctxt(&self) -> String; } pub(crate) fn maybe_msgid_msgctxt_eot_split<'a>( msgid: &'a str, msgctxt: &Option, ) -> Cow<'a, str> { if let Some(ctx) = msgctxt { let mut ret = String::from(ctx); ret.reserve(msgid.len() + 1); ret.push('\u{4}'); ret.push_str(msgid); ret.into() } else { msgid.into() } } fn metadata_msgstr_formatter( msgstr: &str, _: &str, _: usize, ) -> String { let mut ret = String::from("msgstr \"\"\n"); for line in msgstr.lines() { ret.push('"'); ret.push_str(&escape(line)); ret.push_str(r"\n"); ret.push('"'); ret.push('\n'); } ret } fn default_mo_entry_msgstr_formatter( msgstr: &str, delflag: &str, wrapwidth: usize, ) -> String { POStringField::new( "msgstr", delflag, msgstr.trim_end(), "", wrapwidth, ) .to_string() } fn mo_entry_to_string_with_msgstr_formatter( entry: &MOEntry, wrapwidth: usize, delflag: &str, msgstr_formatter: &dyn Fn(&str, &str, usize) -> String, ) -> String { let mut ret = String::new(); if let Some(msgctxt) = &entry.msgctxt { ret.push_str( &POStringField::new( "msgctxt", delflag, msgctxt, "", wrapwidth, ) .to_string(), ); } ret.push_str( &POStringField::new( "msgid", delflag, &entry.msgid, "", wrapwidth, ) .to_string(), ); if let Some(msgid_plural) = &entry.msgid_plural { ret.push_str( &POStringField::new( "msgid_plural", delflag, msgid_plural, "", wrapwidth, ) .to_string(), ); } if entry.msgstr_plural.is_empty() { let msgstr = match &entry.msgstr { Some(msgstr) => msgstr, None => "", }; let formatted_msgstr = msgstr_formatter(msgstr, delflag, wrapwidth); ret.push_str(&formatted_msgstr); } else { for (i, msgstr_plural) in entry.msgstr_plural.iter().enumerate() { ret.push_str( &POStringField::new( "msgstr", delflag, msgstr_plural, &i.to_string(), wrapwidth, ) .to_string(), ); } } ret } pub(crate) fn mo_entry_to_string( entry: &MOEntry, wrapwidth: usize, delflag: &str, ) -> String { mo_entry_to_string_with_msgstr_formatter( entry, wrapwidth, delflag, &default_mo_entry_msgstr_formatter, ) } /// Converts a metadata wrapped by a [MOEntry] to a string /// representation. /// /// ```rust /// use rspolib::{ /// mofile, /// mo_metadata_entry_to_string, /// }; /// /// let file = mofile("tests-data/all.mo").unwrap(); /// let entry = file.metadata_as_entry(); /// let entry_str = mo_metadata_entry_to_string(&entry); /// /// assert!(entry_str.starts_with("msgid \"\"\nmsgstr \"\"")); /// ``` pub fn mo_metadata_entry_to_string(entry: &MOEntry) -> String { mo_entry_to_string_with_msgstr_formatter( entry, 78, "", &metadata_msgstr_formatter, ) } /// Converts a metadata wrapped by a [POEntry] to a string /// representation. /// /// ```rust /// use rspolib::{ /// pofile, /// po_metadata_entry_to_string, /// }; /// /// let file = pofile("tests-data/all.po").unwrap(); /// let entry = file.metadata_as_entry(); /// let entry_str = po_metadata_entry_to_string(&entry, true); /// /// assert!( /// entry_str.starts_with("#, fuzzy\nmsgid \"\"\nmsgstr \"\"") /// ); /// ``` pub fn po_metadata_entry_to_string( entry: &POEntry, metadata_is_fuzzy: bool, ) -> String { let mut ret = String::new(); if metadata_is_fuzzy { ret.push_str("#, fuzzy\n"); } ret.push_str(&mo_metadata_entry_to_string(&MOEntry::from(entry))); ret } pub(crate) struct POStringField<'a> { fieldname: &'a str, delflag: &'a str, value: &'a str, plural_index: &'a str, wrapwidth: usize, } impl<'a> POStringField<'a> { pub fn new( fieldname: &'a str, delflag: &'a str, value: &'a str, plural_index: &'a str, wrapwidth: usize, ) -> Self { Self { fieldname, delflag, value, plural_index, wrapwidth, } } } #[allow(clippy::needless_lifetimes)] impl<'a> fmt::Display for POStringField<'a> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let mut lines = vec!["".to_string()]; let escaped_value = escape(self.value); let repr_plural_index = match self.plural_index.is_empty() { false => format!("[{}]", self.plural_index), true => "".to_string(), }; // +1 here because of the space between fieldname and value let real_width = UnicodeWidthStr::width(escaped_value.as_ref()) + UnicodeWidthStr::width(self.fieldname) + 1; if real_width > self.wrapwidth { let new_lines = wrap(&escaped_value, self.wrapwidth); lines.extend(new_lines); } else { lines = vec![escaped_value.into_owned()]; } // format first line let mut ret = format!( "{}{}{} \"{}\"\n", self.delflag, self.fieldname, repr_plural_index, &lines.remove(0), ); // format other lines for line in lines { ret.push_str(&format!("{}\"{}\"\n", self.delflag, &line)); } write!(f, "{ret}") } } /// A struct to compare two entries. /// /// ```rust /// use std::cmp::Ordering; /// use rspolib::{POEntry, EntryCmpByOptions}; /// /// let mut entry1 = POEntry::from("msgid 1"); /// let entry2 = POEntry::from("msgid 2"); /// /// let compare_by_all_fields = EntryCmpByOptions::new(); /// let compare_by_msgid_only = EntryCmpByOptions::new() /// .by_all(false) /// .by_msgid(true); /// /// assert_eq!(entry1.cmp_by(&entry2, &compare_by_msgid_only), Ordering::Less); /// assert_eq!(entry2.cmp_by(&entry1, &compare_by_msgid_only), Ordering::Greater); /// /// entry1.msgid = "msgid 2".to_string(); /// assert_eq!(entry1.cmp_by(&entry2, &compare_by_msgid_only), Ordering::Equal); /// /// entry1.msgstr = Some("msgstr 1".to_string()); /// assert_eq!(entry1.cmp_by(&entry2, &compare_by_msgid_only), Ordering::Equal); /// assert_eq!(entry1.cmp_by(&entry2, &compare_by_all_fields), Ordering::Greater); /// ``` pub struct EntryCmpByOptions { by_msgid: bool, by_msgstr: bool, by_msgctxt: bool, by_obsolete: bool, by_occurrences: bool, by_msgid_plural: bool, by_msgstr_plural: bool, by_flags: bool, } impl EntryCmpByOptions { /// Creates a instance of [EntryCmpByOptions] with comparisons for all fields enabled pub fn new() -> Self { Self { by_msgid: true, by_msgstr: true, by_msgctxt: true, by_obsolete: true, by_occurrences: true, by_msgid_plural: true, by_msgstr_plural: true, by_flags: true, } } pub fn by_msgid(mut self, by_msgid: bool) -> Self { self.by_msgid = by_msgid; self } pub fn by_msgstr(mut self, by_msgstr: bool) -> Self { self.by_msgstr = by_msgstr; self } pub fn by_msgctxt(mut self, by_msgctxt: bool) -> Self { self.by_msgctxt = by_msgctxt; self } pub fn by_obsolete(mut self, by_obsolete: bool) -> Self { self.by_obsolete = by_obsolete; self } pub fn by_occurrences(mut self, by_occurrences: bool) -> Self { self.by_occurrences = by_occurrences; self } pub fn by_msgid_plural(mut self, by_msgid_plural: bool) -> Self { self.by_msgid_plural = by_msgid_plural; self } pub fn by_msgstr_plural( mut self, by_msgstr_plural: bool, ) -> Self { self.by_msgstr_plural = by_msgstr_plural; self } pub fn by_flags(mut self, by_flags: bool) -> Self { self.by_flags = by_flags; self } pub fn by_all(mut self, by_all: bool) -> Self { self.by_msgid = by_all; self.by_msgstr = by_all; self.by_msgctxt = by_all; self.by_obsolete = by_all; self.by_occurrences = by_all; self.by_msgid_plural = by_all; self.by_msgstr_plural = by_all; self.by_flags = by_all; self } } impl Default for EntryCmpByOptions { fn default() -> Self { Self::new() } } impl From<&Vec<(String, bool)>> for EntryCmpByOptions { fn from(options: &Vec<(String, bool)>) -> Self { let mut ret = Self::new(); for (key, value) in options { match key.as_str() { "msgid" => ret.by_msgid = *value, "msgstr" => ret.by_msgstr = *value, "msgctxt" => ret.by_msgctxt = *value, "obsolete" => ret.by_obsolete = *value, "occurrences" => ret.by_occurrences = *value, "msgid_plural" => ret.by_msgid_plural = *value, "msgstr_plural" => ret.by_msgstr_plural = *value, "flags" => ret.by_flags = *value, _ => {} } } ret } } rspolib-0.1.1/src/entry/moentry.rs000064400000000000000000000252621046102023000152770ustar 00000000000000use std::cmp::Ordering; use std::fmt; use crate::entry::{ maybe_msgid_msgctxt_eot_split, mo_entry_to_string, EntryCmpByOptions, MsgidEotMsgctxt, POEntry, Translated, }; use crate::traits::Merge; /// MO file entry representing a message /// /// Unlike PO files, MO files contain only the content /// needed to translate a program at runtime, so this /// is struct optimized as saves much more memory /// than [POEntry]. /// /// MO entries ieally contain `msgstr` or the fields /// `msgid_plural` and `msgstr_plural` as not being `None`. /// The logic would be: /// /// - If `msgstr` is not `None`, then the entry is a /// translation of a singular form. /// - If `msgid_plural` is not `None`, then the entry /// is a translation of a plural form contained in /// `msgstr_plural`. #[derive(Default, Clone, Debug, PartialEq)] pub struct MOEntry { /// untranslated string pub msgid: String, /// translated string pub msgstr: Option, /// untranslated string for plural form pub msgid_plural: Option, /// translated strings for plural form pub msgstr_plural: Vec, /// context pub msgctxt: Option, } impl MOEntry { pub fn new( msgid: String, msgstr: Option, msgid_plural: Option, msgstr_plural: Vec, msgctxt: Option, ) -> MOEntry { MOEntry { msgid, msgstr, msgid_plural, msgstr_plural, msgctxt, } } /// Convert to a string representation with a given wrap width pub fn to_string_with_wrapwidth( &self, wrapwidth: usize, ) -> String { mo_entry_to_string(self, wrapwidth, "") } /// Compare the current entry with other entry /// /// You can disable some comparison options by setting the corresponding /// field in `options` to `false`. See [EntryCmpByOptions]. pub fn cmp_by( &self, other: &Self, options: &EntryCmpByOptions, ) -> Ordering { let placeholder = &"\0".to_string(); if options.by_msgctxt { let msgctxt = self .msgctxt .as_ref() .unwrap_or(placeholder) .to_string(); let other_msgctxt = other .msgctxt .as_ref() .unwrap_or(placeholder) .to_string(); if msgctxt > other_msgctxt { return Ordering::Greater; } else if msgctxt < other_msgctxt { return Ordering::Less; } } if options.by_msgid_plural { let msgid_plural = self .msgid_plural .as_ref() .unwrap_or(placeholder) .to_string(); let other_msgid_plural = other .msgid_plural .as_ref() .unwrap_or(placeholder) .to_string(); if msgid_plural > other_msgid_plural { return Ordering::Greater; } else if msgid_plural < other_msgid_plural { return Ordering::Less; } } if options.by_msgstr_plural { let mut msgstr_plural = self.msgstr_plural.clone(); msgstr_plural.sort(); let mut other_msgstr_plural = other.msgstr_plural.clone(); other_msgstr_plural.sort(); if msgstr_plural > other_msgstr_plural { return Ordering::Greater; } else if msgstr_plural < other_msgstr_plural { return Ordering::Less; } } if options.by_msgid { if self.msgid > other.msgid { return Ordering::Greater; } else if self.msgid < other.msgid { return Ordering::Less; } } if options.by_msgstr { let msgstr = self .msgstr .as_ref() .unwrap_or(placeholder) .to_string(); let other_msgstr = other .msgstr .as_ref() .unwrap_or(placeholder) .to_string(); if msgstr > other_msgstr { return Ordering::Greater; } else if msgstr < other_msgstr { return Ordering::Less; } } Ordering::Equal } } impl MsgidEotMsgctxt for MOEntry { fn msgid_eot_msgctxt(&self) -> String { maybe_msgid_msgctxt_eot_split(&self.msgid, &self.msgctxt) .to_string() } } impl Translated for MOEntry { /// Returns `true` if the entry is translated /// /// Really, MO files has only translated entries, /// but this function is here to be consistent /// with the PO implementation and to be used /// when manipulating MOEntry directly. fn translated(&self) -> bool { if let Some(msgstr) = &self.msgstr { return !msgstr.is_empty(); } if self.msgstr_plural.is_empty() { return false; } else { for msgstr_plural in &self.msgstr_plural { if !msgstr_plural.is_empty() { return true; } } } false } } impl Merge for MOEntry { fn merge(&mut self, other: Self) { self.msgid = other.msgid; self.msgstr = other.msgstr; self.msgid_plural = other.msgid_plural; self.msgstr_plural = other.msgstr_plural; self.msgctxt = other.msgctxt; } } impl fmt::Display for MOEntry { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.to_string_with_wrapwidth(78)) } } impl From<&str> for MOEntry { /// Generates a [MOEntry] from a string as the `msgid` fn from(s: &str) -> Self { MOEntry::new(s.to_string(), None, None, vec![], None) } } impl From<&POEntry> for MOEntry { /// Generates a [MOEntry] from a [POEntry] /// /// Keep in mind that this conversion loss the information /// that is contained in [POEntry]s but not in [MOEntry]s. fn from(entry: &POEntry) -> Self { MOEntry { msgid: entry.msgid.clone(), msgstr: entry.msgstr.clone(), msgid_plural: entry.msgid_plural.clone(), msgstr_plural: entry.msgstr_plural.clone(), msgctxt: entry.msgctxt.clone(), } } } #[cfg(test)] mod tests { use super::*; #[test] fn constructor() { let moentry = MOEntry::new( "msgid".to_string(), Some("msgstr".to_string()), None, vec![], None, ); assert_eq!(moentry.msgid, "msgid"); assert_eq!(moentry.msgstr, Some("msgstr".to_string())); assert_eq!(moentry.msgid_plural, None); assert_eq!(moentry.msgstr_plural, vec![] as Vec); assert_eq!(moentry.msgctxt, None); } #[test] fn moentry_translated() { // empty msgstr means untranslated let moentry = MOEntry::new( "msgid".to_string(), Some("".to_string()), None, vec![], None, ); assert_eq!(moentry.translated(), false); let moentry = MOEntry::new( "msgid".to_string(), Some("msgstr".to_string()), None, vec![], None, ); assert_eq!(moentry.translated(), true); // empty msgstr_plural means untranslated let moentry = MOEntry::new( "msgid".to_string(), None, None, vec![], None, ); assert_eq!(moentry.translated(), false); // empty msgstr in msgstr_plural means untranslated let moentry = MOEntry::new( "msgid".to_string(), None, None, vec!["".to_string()], None, ); assert_eq!(moentry.translated(), false); } #[test] fn moentry_merge() { let mut moentry = MOEntry::new( "msgid".to_string(), Some("msgstr".to_string()), Some("msgid_plural".to_string()), vec!["msgstr_plural".to_string()], Some("msgctxt".to_string()), ); let other = MOEntry::new( "other_msgid".to_string(), Some("other_msgstr".to_string()), Some("other_msgid_plural".to_string()), vec!["other_msgstr_plural".to_string()], Some("other_msgctxt".to_string()), ); moentry.merge(other); assert_eq!(moentry.msgid, "other_msgid"); assert_eq!(moentry.msgstr, Some("other_msgstr".to_string())); assert_eq!( moentry.msgid_plural, Some("other_msgid_plural".to_string()) ); assert_eq!( moentry.msgstr_plural, vec!["other_msgstr_plural".to_string()], ); assert_eq!( moentry.msgctxt, Some("other_msgctxt".to_string()) ); } #[test] fn moentry_to_string() { // with msgid_plural let moentry = MOEntry::new( "msgid".to_string(), Some("msgstr".to_string()), Some("msgid_plural".to_string()), vec!["msgstr_plural".to_string()], Some("msgctxt".to_string()), ); let expected = r#"msgctxt "msgctxt" msgid "msgid" msgid_plural "msgid_plural" msgstr[0] "msgstr_plural" "# .to_string(); assert_eq!(moentry.to_string(), expected); // with msgstr let moentry = MOEntry::new( "msgid".to_string(), Some("msgstr".to_string()), None, vec![], Some("msgctxt".to_string()), ); let expected = r#"msgctxt "msgctxt" msgid "msgid" msgstr "msgstr" "# .to_string(); assert_eq!(moentry.to_string(), expected); } #[test] fn moentry_from_poentry() { let msgstr_plural = vec!["msgstr_plural".to_string()]; let mut poentry = POEntry::new(0); poentry.msgid = "msgid".to_string(); poentry.msgstr = Some("msgstr".to_string()); poentry.msgid_plural = Some("msgid_plural".to_string()); poentry.msgstr_plural = msgstr_plural.clone(); poentry.msgctxt = Some("msgctxt".to_string()); let moentry = MOEntry::from(&poentry); assert_eq!(moentry.msgid, "msgid"); assert_eq!(moentry.msgstr, Some("msgstr".to_string())); assert_eq!( moentry.msgid_plural, Some("msgid_plural".to_string()) ); assert_eq!(moentry.msgstr_plural, msgstr_plural); assert_eq!(moentry.msgctxt, Some("msgctxt".to_string())); } } rspolib-0.1.1/src/entry/poentry.rs000064400000000000000000001037121046102023000152770ustar 00000000000000use std::cmp::Ordering; use std::fmt; use unicode_width::UnicodeWidthStr; use crate::entry::{ maybe_msgid_msgctxt_eot_split, mo_entry_to_string, EntryCmpByOptions, MOEntry, MsgidEotMsgctxt, POStringField, Translated, }; use crate::errors::EscapingError; use crate::escaping::unescape; use crate::traits::Merge; use crate::twrapper::wrap; /// PO file entry representing a message /// /// This struct contains all the information that is stored /// in PO files. /// /// PO entries can contain `msgstr` or the fields /// `msgid_plural` and `msgstr_plural` as not being `None`. /// The logic would be: /// /// - If `msgstr` is not `None`, then the entry is a /// translation of a singular form. /// - If `msgid_plural` is not `None`, then the entry /// is a translation of a plural form contained in /// `msgstr_plural`. /// /// The `previous_msgid` and `previous_msgid_plural` fields /// are used to store the previous `msgid` and `msgid_plural` /// values when the entry is obsolete. /// /// The `previous_msgctxt` field is used to store the previous /// `msgctxt` value when the entry is obsolete. #[derive(Default, Clone, PartialEq, Debug)] pub struct POEntry { /// untranslated string pub msgid: String, /// translated string pub msgstr: Option, /// untranslated string for plural form pub msgid_plural: Option, /// translated strings for plural form pub msgstr_plural: Vec, /// context pub msgctxt: Option, /// the entry is marked as obsolete pub obsolete: bool, /// generated comments for machines pub comment: Option, /// generated comments for translators pub tcomment: Option, /// files and lines from which the translations are taken pub occurrences: Vec<(String, String)>, /// flags indicating the state, i.e. fuzzy pub flags: Vec, /// previous untranslated string pub previous_msgid: Option, /// previous untranslated string for plural form pub previous_msgid_plural: Option, /// previous context pub previous_msgctxt: Option, /// line number in the file or content pub linenum: usize, } impl POEntry { /// Creates a new POEntry /// /// It just creates the entry with a given line number. /// This function is used by the parser to initialize new /// entries. Use the `From` traits instead to initialize /// [POEntry]s from strings. pub fn new(linenum: usize) -> Self { Self { msgid: String::new(), linenum, ..Default::default() } } /// Returns `true` the entry has the `fuzzy` flag pub fn fuzzy(&self) -> bool { self.flags.contains(&"fuzzy".to_string()) } fn format_comment_inplace( &self, comment: &str, prefix: &str, wrapwidth: usize, target: &mut String, ) { for line in comment.lines() { if UnicodeWidthStr::width(line) + 2 > wrapwidth { target.push_str(&wrap(line, wrapwidth - 2).join("\n")) } else { target.push_str(prefix); target.push_str(line); } target.push('\n'); } } /// Convert to string with a given wrap width pub fn to_string_with_wrapwidth( &self, wrapwidth: usize, ) -> String { let mut ret = String::new(); // translator comments if let Some(tcomment) = &self.tcomment { self.format_comment_inplace( tcomment, "# ", wrapwidth, &mut ret, ); } // comments if let Some(comment) = &self.comment { self.format_comment_inplace( comment, "#. ", wrapwidth, &mut ret, ); } // occurrences if !self.obsolete && !self.occurrences.is_empty() { let whitespace_sep_occurrences = self .occurrences .iter() .map(|(fpath, lineno)| { if lineno.is_empty() { return fpath.clone(); } format!("{fpath}:{lineno}") }) .collect::>(); let mut files_repr: Vec = vec![]; let mut current_line_occs: Vec<&str> = vec![]; let mut current_width = 2; for occ in &whitespace_sep_occurrences { let occ_width = UnicodeWidthStr::width(occ.as_str()); let width = current_width + occ_width + 1; if width > wrapwidth && !current_line_occs.is_empty() { let curr_line = format!("#: {}", current_line_occs.join(" ")); files_repr.push(curr_line); current_width = occ_width + 2; } else { current_line_occs.push(occ); current_width += occ_width + 1; } } if !current_line_occs.is_empty() { let curr_line = format!("#: {}", current_line_occs.join(" ")); files_repr.push(curr_line); } ret.push_str(&files_repr.join("\n")); ret.push('\n'); } // flags if !self.flags.is_empty() { ret.push_str(&format!("#, {}\n", self.flags.join(", "))); } // previous context and previous msgid/msgid_plural let mut prefix = String::from("#"); if self.obsolete { prefix.push('~'); } prefix.push_str("| "); if let Some(previous_msgctxt) = &self.previous_msgctxt { ret.push_str( &POStringField::new( "msgctxt", &prefix, previous_msgctxt, "", wrapwidth, ) .to_string(), ); } if let Some(previous_msgid) = &self.previous_msgid { ret.push_str( &POStringField::new( "msgid", &prefix, previous_msgid, "", wrapwidth, ) .to_string(), ); } if let Some(previous_msgid_plural) = &self.previous_msgid_plural { ret.push_str( &POStringField::new( "msgid", &prefix, previous_msgid_plural, "", wrapwidth, ) .to_string(), ); ret.push('\n'); } ret.push_str(&mo_entry_to_string( &MOEntry::from(self), wrapwidth, match self.obsolete { true => "#~ ", false => "", }, )); ret } pub fn unescaped(&self) -> Result { let mut entry = self.clone(); entry.msgid = unescape(&self.msgid)?.to_string(); if let Some(msgstr) = &self.msgstr { entry.msgstr = Some(unescape(msgstr)?.to_string()); } if let Some(msgid_plural) = &self.msgid_plural { entry.msgid_plural = Some(unescape(msgid_plural)?.to_string()); } for msgstr_plural in &mut entry.msgstr_plural { *msgstr_plural = unescape(msgstr_plural)?.to_string(); } if let Some(msgctxt) = &self.msgctxt { entry.msgctxt = Some(unescape(msgctxt)?.to_string()); } if let Some(previous_msgid) = &self.previous_msgid { entry.previous_msgid = Some(unescape(previous_msgid)?.to_string()); } if let Some(previous_msgid_plural) = &self.previous_msgid_plural { entry.previous_msgid_plural = Some(unescape(previous_msgid_plural)?.to_string()); } if let Some(previous_msgctxt) = &self.previous_msgctxt { entry.previous_msgctxt = Some(unescape(previous_msgctxt)?.to_string()); } Ok(entry) } /// Compare the current entry with other entry /// /// You can disable some comparison options by setting the corresponding /// field in `options` to `false`. See [EntryCmpByOptions]. pub fn cmp_by( &self, other: &Self, options: &EntryCmpByOptions, ) -> Ordering { if options.by_obsolete && self.obsolete != other.obsolete { match self.obsolete { true => return Ordering::Less, false => return Ordering::Greater, } } if options.by_occurrences { let mut occ1 = self.occurrences.clone(); occ1.sort(); let mut occ2 = other.occurrences.clone(); occ2.sort(); if occ1 > occ2 { return Ordering::Greater; } else if occ1 < occ2 { return Ordering::Less; } } if options.by_flags { let mut flags1 = self.flags.clone(); flags1.sort(); let mut flags2 = other.flags.clone(); flags2.sort(); if flags1 > flags2 { return Ordering::Greater; } else if flags1 < flags2 { return Ordering::Less; } } let placeholder = &"\0".to_string(); if options.by_msgctxt { let msgctxt = self .msgctxt .as_ref() .unwrap_or(placeholder) .to_string(); let other_msgctxt = other .msgctxt .as_ref() .unwrap_or(placeholder) .to_string(); if msgctxt > other_msgctxt { return Ordering::Greater; } else if msgctxt < other_msgctxt { return Ordering::Less; } } if options.by_msgid_plural { let msgid_plural = self .msgid_plural .as_ref() .unwrap_or(placeholder) .to_string(); let other_msgid_plural = other .msgid_plural .as_ref() .unwrap_or(placeholder) .to_string(); if msgid_plural > other_msgid_plural { return Ordering::Greater; } else if msgid_plural < other_msgid_plural { return Ordering::Less; } } if options.by_msgstr_plural { let mut msgstr_plural = self.msgstr_plural.clone(); msgstr_plural.sort(); let mut other_msgstr_plural = other.msgstr_plural.clone(); other_msgstr_plural.sort(); if msgstr_plural > other_msgstr_plural { return Ordering::Greater; } else if msgstr_plural < other_msgstr_plural { return Ordering::Less; } } if options.by_msgid { if self.msgid > other.msgid { return Ordering::Greater; } else if self.msgid < other.msgid { return Ordering::Less; } } if options.by_msgstr { let msgstr = self .msgstr .as_ref() .unwrap_or(placeholder) .to_string(); let other_msgstr = other .msgstr .as_ref() .unwrap_or(placeholder) .to_string(); if msgstr > other_msgstr { return Ordering::Greater; } else if msgstr < other_msgstr { return Ordering::Less; } } Ordering::Equal } } impl MsgidEotMsgctxt for POEntry { fn msgid_eot_msgctxt(&self) -> String { maybe_msgid_msgctxt_eot_split(&self.msgid, &self.msgctxt) .to_string() } } impl Translated for POEntry { fn translated(&self) -> bool { if self.obsolete || self.fuzzy() { return false; } if let Some(msgstr) = &self.msgstr { return !msgstr.is_empty(); } if self.msgstr_plural.is_empty() { return false; } for msgstr in &self.msgstr_plural { if msgstr.is_empty() { return false; } } true } } impl Merge for POEntry { fn merge(&mut self, other: Self) { self.msgid = other.msgid; self.msgstr = other.msgstr; self.msgid_plural = other.msgid_plural; self.msgstr_plural = other.msgstr_plural; self.msgctxt = other.msgctxt; self.obsolete = other.obsolete; self.comment = other.comment; self.tcomment = other.tcomment; self.occurrences = other.occurrences; self.flags = other.flags; self.previous_msgctxt = other.previous_msgctxt; self.previous_msgid = other.previous_msgid; self.previous_msgid_plural = other.previous_msgid_plural; self.linenum = other.linenum; } } impl fmt::Display for POEntry { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.to_string_with_wrapwidth(78)) } } impl From<&str> for POEntry { fn from(s: &str) -> Self { let mut entry = POEntry::new(0); entry.msgid = s.to_string(); entry } } impl From for POEntry { fn from(linenum: usize) -> Self { Self::new(linenum) } } impl From<(&str, &str)> for POEntry { fn from((msgid, msgstr): (&str, &str)) -> Self { let mut entry = POEntry::new(0); entry.msgid = msgid.to_string(); entry.msgstr = Some(msgstr.to_string()); entry } } impl From<&MOEntry> for POEntry { fn from(mo_entry: &MOEntry) -> Self { let mut entry = POEntry::new(0); entry.msgid = mo_entry.msgid.clone(); entry.msgstr = mo_entry.msgstr.as_ref().cloned(); entry.msgid_plural = mo_entry.msgid_plural.as_ref().cloned(); entry.msgstr_plural = mo_entry.msgstr_plural.clone(); entry.msgctxt = mo_entry.msgctxt.as_ref().cloned(); entry } } #[cfg(test)] mod tests { use super::*; use crate::pofile; #[test] fn constructor() { let poentry = POEntry::new(7); assert_eq!(poentry.linenum, 7); assert_eq!(poentry.msgid, ""); assert_eq!(poentry.msgstr, None); assert_eq!(poentry.msgid_plural, None); assert_eq!(poentry.msgstr_plural, vec![] as Vec); assert_eq!(poentry.msgctxt, None); } #[test] fn fuzzy() { let non_fuzzy_entry = POEntry::new(0); assert_eq!(non_fuzzy_entry.fuzzy(), false); let mut fuzzy_entry = POEntry::new(0); fuzzy_entry.flags.push("fuzzy".to_string()); assert_eq!(fuzzy_entry.fuzzy(), true); } #[test] fn translated() { // obsolete means untranslated let mut obsolete_entry = POEntry::new(0); obsolete_entry.obsolete = true; assert_eq!(obsolete_entry.translated(), false); // fuzzy means untranslated let mut fuzzy_entry = POEntry::new(0); fuzzy_entry.flags.push("fuzzy".to_string()); assert_eq!(fuzzy_entry.translated(), false); // no msgstr means untranslated let no_msgstr_entry = POEntry::new(0); assert_eq!(no_msgstr_entry.translated(), false); // empty msgstr means untranslated let mut empty_msgstr_entry = POEntry::new(0); empty_msgstr_entry.msgstr = Some("".to_string()); assert_eq!(empty_msgstr_entry.translated(), false); // with msgstr means translated let mut translated_entry = POEntry::new(0); translated_entry.msgstr = Some("msgstr".to_string()); assert_eq!(translated_entry.translated(), true); // empty msgstr_plural means untranslated let mut empty_msgstr_plural_entry = POEntry::new(0); empty_msgstr_plural_entry.msgstr_plural = vec![]; assert_eq!(empty_msgstr_plural_entry.translated(), false); // with empty msgstr_plural means untranslated let mut empty_msgstr_plural_entry = POEntry::new(0); empty_msgstr_plural_entry.msgstr_plural = vec!["".to_string()]; assert_eq!(empty_msgstr_plural_entry.translated(), false); // with msgstr_plural means translated let mut translated_plural_entry = POEntry::new(0); translated_plural_entry.msgstr_plural = vec!["msgstr_plural".to_string()]; assert_eq!(translated_plural_entry.translated(), true); } #[test] fn merge() { let mut poentry = POEntry::new(0); poentry.msgid = "msgid".to_string(); poentry.msgstr = Some("msgstr".to_string()); poentry.msgid_plural = Some("msgid_plural".to_string()); poentry.msgstr_plural = vec!["msgstr_plural".to_string()]; let mut other = POEntry::new(0); other.msgid = "other_msgid".to_string(); other.msgstr = Some("other_msgstr".to_string()); other.msgid_plural = Some("other_msgid_plural".to_string()); other.msgstr_plural = vec!["other_msgstr_plural".to_string()]; poentry.merge(other); assert_eq!(poentry.msgid, "other_msgid"); assert_eq!(poentry.msgstr, Some("other_msgstr".to_string())); assert_eq!( poentry.msgid_plural, Some("other_msgid_plural".to_string()) ); assert_eq!( poentry.msgstr_plural, vec!["other_msgstr_plural".to_string()] ); } #[test] fn to_string() { let mut entry = POEntry::new(0); // empty let expected = "msgid \"\"\nmsgstr \"\"\n".to_string(); assert_eq!(entry.to_string(), expected); // msgid entry.msgid = "msgid".to_string(); let expected = "msgid \"msgid\"\nmsgstr \"\"\n".to_string(); assert_eq!(entry.to_string(), expected); // msgstr entry.msgstr = Some("msgstr".to_string()); let expected = concat!("msgid \"msgid\"\n", "msgstr \"msgstr\"\n"); assert_eq!(entry.to_string(), expected); // msgid_plural entry.msgid_plural = Some("msgid_plural".to_string()); let expected = concat!( "msgid \"msgid\"\n", "msgid_plural \"msgid_plural\"\n", "msgstr \"msgstr\"\n", ); assert_eq!(entry.to_string(), expected); // msgid_plural (no msgstr) entry.msgstr = None; let expected = concat!( "msgid \"msgid\"\n", "msgid_plural \"msgid_plural\"\n", "msgstr \"\"\n", ); assert_eq!(entry.to_string(), expected); // msgstr_plural entry.msgstr_plural = vec!["plural 1".to_string(), "plural 2".to_string()]; let expected = concat!( "msgid \"msgid\"\nmsgid_plural \"msgid_plural\"\n", "msgstr[0] \"plural 1\"\nmsgstr[1] \"plural 2\"\n", ); assert_eq!(entry.to_string(), expected); // msgctxt entry.msgctxt = Some("msgctxt".to_string()); let expected = concat!( "msgctxt \"msgctxt\"\nmsgid \"msgid\"\n", "msgid_plural \"msgid_plural\"\n", "msgstr[0] \"plural 1\"\n", "msgstr[1] \"plural 2\"\n" ); assert_eq!(entry.to_string(), expected); // flags entry.flags.push("fuzzy".to_string()); let expected = concat!( "#, fuzzy\n", "msgctxt \"msgctxt\"\nmsgid \"msgid\"\n", "msgid_plural \"msgid_plural\"\n", "msgstr[0] \"plural 1\"\n", "msgstr[1] \"plural 2\"\n" ); assert_eq!(entry.to_string(), expected); entry.flags.push("python-format".to_string()); let expected = concat!( "#, fuzzy, python-format\nmsgctxt \"msgctxt\"\n", "msgid \"msgid\"\nmsgid_plural \"msgid_plural\"\n", "msgstr[0] \"plural 1\"\nmsgstr[1] \"plural 2\"\n" ); assert_eq!(entry.to_string(), expected); // comments entry.comment = Some("comment".to_string()); let expected = concat!( "#. comment\n#, fuzzy, python-format\n", "msgctxt \"msgctxt\"\nmsgid \"msgid\"\n", "msgid_plural \"msgid_plural\"\n", "msgstr[0] \"plural 1\"\nmsgstr[1] \"plural 2\"\n" ); assert_eq!(entry.to_string(), expected); entry.tcomment = Some("translator comment".to_string()); let expected = concat!( "# translator comment\n#. comment\n", "#, fuzzy, python-format\nmsgctxt \"msgctxt\"\n", "msgid \"msgid\"\nmsgid_plural \"msgid_plural\"\n", "msgstr[0] \"plural 1\"\nmsgstr[1] \"plural 2\"\n" ); assert_eq!(entry.to_string(), expected); // obsolete entry.obsolete = true; let expected = concat!( "# translator comment\n#. comment\n", "#, fuzzy, python-format\n#~ msgctxt \"msgctxt\"\n", "#~ msgid \"msgid\"\n", "#~ msgid_plural \"msgid_plural\"\n", "#~ msgstr[0] \"plural 1\"\n", "#~ msgstr[1] \"plural 2\"\n" ); assert_eq!(entry.to_string(), expected); // occurrences // // when obsolete, occurrences are not included entry .occurrences .push(("file1.rs".to_string(), "1".to_string())); entry .occurrences .push(("file2.rs".to_string(), "2".to_string())); let expected = concat!( "# translator comment\n#. comment\n", "#, fuzzy, python-format\n", "#~ msgctxt \"msgctxt\"\n", "#~ msgid \"msgid\"\n", "#~ msgid_plural \"msgid_plural\"\n", "#~ msgstr[0] \"plural 1\"\n", "#~ msgstr[1] \"plural 2\"\n" ); assert_eq!(entry.to_string(), expected); entry.obsolete = false; let expected = concat!( "# translator comment\n#. comment\n", "#: file1.rs:1 file2.rs:2\n", "#, fuzzy, python-format\n", "msgctxt \"msgctxt\"\nmsgid \"msgid\"\n", "msgid_plural \"msgid_plural\"\n", "msgstr[0] \"plural 1\"\n", "msgstr[1] \"plural 2\"\n" ); assert_eq!(entry.to_string(), expected); // Basic complete example entry.msgstr = Some("msgstr".to_string()); entry.comment = Some("comment".to_string()); entry.tcomment = Some("translator comment".to_string()); entry.flags.push("rspolib".to_string()); let expected = concat!( "# translator comment\n#. comment\n", "#: file1.rs:1 file2.rs:2\n", "#, fuzzy, python-format, rspolib\n", "msgctxt \"msgctxt\"\nmsgid \"msgid\"\n", "msgid_plural \"msgid_plural\"\n", "msgstr[0] \"plural 1\"\n", "msgstr[1] \"plural 2\"\n" ); assert_eq!(entry.to_string(), expected); // previous msgctxt entry.previous_msgctxt = Some("A previous msgctxt".to_string()); let expected = concat!( "# translator comment\n#. comment\n", "#: file1.rs:1 file2.rs:2\n", "#, fuzzy, python-format, rspolib\n", "#| msgctxt \"A previous msgctxt\"\n", "msgctxt \"msgctxt\"\n", "msgid \"msgid\"\n", "msgid_plural \"msgid_plural\"\n", "msgstr[0] \"plural 1\"\n", "msgstr[1] \"plural 2\"\n" ); assert_eq!(entry.to_string(), expected); // previous msgid entry.previous_msgid = Some("A previous msgid".to_string()); let expected = concat!( "# translator comment\n#. comment\n", "#: file1.rs:1 file2.rs:2\n", "#, fuzzy, python-format, rspolib\n", "#| msgctxt \"A previous msgctxt\"\n", "#| msgid \"A previous msgid\"\n", "msgctxt \"msgctxt\"\n", "msgid \"msgid\"\n", "msgid_plural \"msgid_plural\"\n", "msgstr[0] \"plural 1\"\n", "msgstr[1] \"plural 2\"\n" ); assert_eq!(entry.to_string(), expected); } #[test] fn multiline_format() { let mut entry = POEntry::new(0); // simple msgid wrapping entry.msgid = concat!( " A long long long long long long long long", " long long long long long long long msgid", ) .to_string(); let expected = concat!( "msgid \"\"\n", "\" A long long long long long long long long long", " long long long long long \"\n", "\"long msgid\"\n", "msgstr \"\"\n", ); assert_eq!(entry.to_string(), expected); entry.msgid = concat!( "A long long long long long long long long", " long long long long long long long long long", " long long long long long long long long long", " long long long long long long long long long", " long long long long long long long long long", " msgid", ) .to_string(); let expected = concat!( "msgid \"\"\n", "\"A long long long long long long", " long long long long long long long long long \"\n", "\"long long long long long long long long long long", " long long long long long \"\n\"long long long long", " long long long long long long long long long long", " msgid\"\n", "msgstr \"\"\n", ); assert_eq!(entry.to_string(), expected); // include newlines in msgid entry.msgid = concat!( "A long long long long\nlong long long long\n", "long long long\nlong long long long lo\nng long", " msgid", ) .to_string(); let expected = concat!( "msgid \"\"\n", "\"A long long long long\\nlong long long long\\n", "long long long\\nlong long long \"\n", "\"long lo\\nng long msgid\"\n", "msgstr \"\"\n" ); assert_eq!(entry.to_string(), expected); } #[test] fn format_escapes() { let mut entry = POEntry::new(0); // " entry.msgid = "aa\"bb".to_string(); assert_eq!( entry.to_string(), "msgid \"aa\\\"bb\"\nmsgstr \"\"\n", ); // \n entry.msgid = "aa\nbb".to_string(); assert_eq!( entry.to_string(), "msgid \"aa\\nbb\"\nmsgstr \"\"\n", ); // \t entry.msgid = "aa\tbb".to_string(); assert_eq!( entry.to_string(), "msgid \"aa\\tbb\"\nmsgstr \"\"\n", ); // \r entry.msgid = "aa\rbb".to_string(); assert_eq!( entry.to_string(), "msgid \"aa\\rbb\"\nmsgstr \"\"\n", ); // \\ entry.msgid = "aa\\bb".to_string(); assert_eq!( entry.to_string(), "msgid \"aa\\\\bb\"\nmsgstr \"\"\n", ); } #[test] fn format_wrapping() { let path = "tests-data/wrapping.po"; let file = pofile(path).unwrap(); let expected = concat!( "# test wrapping\n", "msgid \"\"\n", "msgstr \"\"\n", "\n", "msgid \"This line will not be wrapped\"\n", "msgstr \"\"\n", "\nmsgid \"\"\n", "\"Some line that contain special characters", " \\\" and that \\t is very, very, very \"\n", "\"long...: %s \\n\"\n", "msgstr \"\"\n", "\nmsgid \"\"\n", "\"Some line that contain special characters", " \\\"foobar\\\" and that contains \"\n", "\"whitespace at the end \"\n", "msgstr \"\"\n" ); assert_eq!(file.to_string(), expected); } #[test] fn cmp_by_obsolete() { let mut entry1 = POEntry::new(0); let mut entry2 = POEntry::new(0); // options let by_obsolete = EntryCmpByOptions::new().by_all(false).by_obsolete(true); let by_nothing = EntryCmpByOptions::new().by_all(false); entry1.obsolete = true; entry2.obsolete = false; assert_eq!( entry1.cmp_by(&entry2, &by_obsolete), Ordering::Less, ); assert_eq!( entry1.cmp_by(&entry2, &by_nothing), Ordering::Equal, ); assert_eq!( entry2.cmp_by(&entry1, &by_obsolete), Ordering::Greater, ); entry1.obsolete = false; assert_eq!( entry1.cmp_by(&entry2, &by_obsolete), Ordering::Equal, ); } #[test] fn cmp_by_msgid() { let mut entry1 = POEntry::new(0); let mut entry2 = POEntry::new(0); // options let by_msgid = EntryCmpByOptions::new().by_all(false).by_msgid(true); let by_nothing = EntryCmpByOptions::new().by_all(false); entry1.msgid = "a".to_string(); entry2.msgid = "b".to_string(); assert_eq!(entry1.cmp_by(&entry2, &by_msgid), Ordering::Less); assert_eq!( entry2.cmp_by(&entry1, &by_msgid), Ordering::Greater, ); assert_eq!( entry2.cmp_by(&entry1, &by_nothing), Ordering::Equal, ); } #[test] fn cmp_by_msgstr() { let mut entry1 = POEntry::new(0); let mut entry2 = POEntry::new(0); // options let by_msgstr = EntryCmpByOptions::new().by_all(false).by_msgstr(true); let by_nothing = EntryCmpByOptions::new().by_all(false); entry1.msgstr = Some("a".to_string()); entry2.msgstr = Some("b".to_string()); assert_eq!( entry1.cmp_by(&entry2, &by_msgstr), Ordering::Less ); assert_eq!( entry2.cmp_by(&entry1, &by_msgstr), Ordering::Greater, ); assert_eq!( entry2.cmp_by(&entry1, &by_nothing), Ordering::Equal, ); } #[test] fn cmp_by_msgctxt() { let mut entry1 = POEntry::new(0); let mut entry2 = POEntry::new(0); // options let by_msgctxt = EntryCmpByOptions::new().by_all(false).by_msgctxt(true); let by_nothing = EntryCmpByOptions::new().by_all(false); entry1.msgctxt = Some("a".to_string()); entry2.msgctxt = Some("b".to_string()); assert_eq!( entry1.cmp_by(&entry2, &by_msgctxt), Ordering::Less ); assert_eq!( entry2.cmp_by(&entry1, &by_msgctxt), Ordering::Greater, ); assert_eq!( entry2.cmp_by(&entry1, &by_nothing), Ordering::Equal, ); } #[test] fn cmp_by_msgid_plural() { let mut entry1 = POEntry::new(0); let mut entry2 = POEntry::new(0); // options let by_msgid_plural = EntryCmpByOptions::new() .by_all(false) .by_msgid_plural(true); let by_nothing = EntryCmpByOptions::new().by_all(false); entry1.msgid_plural = Some("a".to_string()); entry2.msgid_plural = Some("b".to_string()); assert_eq!( entry1.cmp_by(&entry2, &by_msgid_plural), Ordering::Less, ); assert_eq!( entry2.cmp_by(&entry1, &by_msgid_plural), Ordering::Greater, ); assert_eq!( entry2.cmp_by(&entry1, &by_nothing), Ordering::Equal, ); } #[test] fn cmp_by_msgstr_plural() { let mut entry1 = POEntry::new(0); let mut entry2 = POEntry::new(0); // options let by_msgstr_plural = EntryCmpByOptions::new() .by_all(false) .by_msgstr_plural(true); let by_nothing = EntryCmpByOptions::new().by_all(false); entry1.msgstr_plural.insert(0, "a".to_string()); entry2.msgstr_plural.insert(0, "b".to_string()); assert_eq!( entry1.cmp_by(&entry2, &by_msgstr_plural), Ordering::Less, ); assert_eq!( entry2.cmp_by(&entry1, &by_msgstr_plural), Ordering::Greater, ); assert_eq!( entry2.cmp_by(&entry1, &by_nothing), Ordering::Equal, ); } #[test] fn cmp_by_flags() { let mut entry1 = POEntry::new(0); let mut entry2 = POEntry::new(0); // options let by_flags = EntryCmpByOptions::new().by_all(false).by_flags(true); let by_nothing = EntryCmpByOptions::new().by_all(false); entry1.flags.push("a".to_string()); entry2.flags.push("b".to_string()); assert_eq!(entry1.cmp_by(&entry2, &by_flags), Ordering::Less); assert_eq!( entry2.cmp_by(&entry1, &by_flags), Ordering::Greater, ); assert_eq!( entry2.cmp_by(&entry1, &by_nothing), Ordering::Equal, ); } #[test] fn cmp_by_occurrences() { let mut entry1 = POEntry::new(0); let mut entry2 = POEntry::new(0); // options let by_occurrences = EntryCmpByOptions::new() .by_all(false) .by_occurrences(true); let by_nothing = EntryCmpByOptions::new().by_all(false); entry1.occurrences.push(("a".to_string(), "30".to_string())); entry2.occurrences.push(("a".to_string(), "40".to_string())); assert_eq!( entry1.cmp_by(&entry2, &by_occurrences), Ordering::Less, ); assert_eq!( entry2.cmp_by(&entry1, &by_occurrences), Ordering::Greater, ); assert_eq!( entry2.cmp_by(&entry1, &by_nothing), Ordering::Equal, ); entry2.occurrences[0] = ("a".to_string(), "30".to_string()); assert_eq!( entry2.cmp_by(&entry1, &by_nothing), Ordering::Equal, ); } } rspolib-0.1.1/src/errors.rs000064400000000000000000000214701046102023000137520ustar 00000000000000//! Errors generated by the parsers //! //! # Complete example //! //! ## Read a PO file (SyntaxError) //! //! ```rust //! use rspolib::{pofile, POFile, errors::SyntaxError}; //! //! let path = "tests-data/unescaped-double-quote-msgid.po"; //! //! let file: Option = match pofile(path) { //! Ok(file) => Some(file), //! Err(e) => match e { //! ref SyntaxError => { //! assert!(e.to_string().ends_with("unescaped double quote found")); //! None //! }, //! }, //! }; //! ``` //! //! ## Read a MO file (IOError) //! //! ```rust //! use rspolib::{mofile, MOFile, errors::IOError, MAGIC}; //! use rspolib_testing::create_binary_content; //! //! let version = 0; //! let data = vec![MAGIC, version]; //! let content = create_binary_content(&data, true); //! //! let file: Option = match mofile(content) { //! Ok(file) => Some(file), //! Err(e) => match e { //! ref IOError => { //! assert!(e.to_string().ends_with("malformed or corrupted data found when parsing number of strings")); //! None //! }, //! }, //! }; //! ``` //! use std::fmt; use snafu::prelude::*; /// A struct to represent a path to a file or a file content #[derive(Debug, PartialEq)] pub struct MaybeFilename { filename: String, filename_is_path: bool, } impl MaybeFilename { pub fn new(filename: &str, filename_is_path: bool) -> Self { Self { filename: filename.to_string(), filename_is_path, } } } impl fmt::Display for MaybeFilename { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if self.filename_is_path { write!(f, " in file {}", self.filename) } else { Ok(()) } } } /// Errors generated when the mo files parser can't parse some content. /// /// # Examples /// /// ## Unsupported MO revision number /// /// ```rust /// use rspolib::{mofile, errors::IOError, MAGIC}; /// use rspolib_testing::create_binary_content; /// /// let version = 234; /// let data = vec![MAGIC, version]; /// let content = create_binary_content(&data, true); /// /// assert_eq!( /// mofile(content), /// Err(IOError::UnsupportedMORevisionNumber { version }), /// ); /// ``` /// /// ## Incorrect magic number /// /// ``` /// use rspolib::{mofile, errors::IOError}; /// use rspolib_testing::create_binary_content; /// /// let magic_number = 800; /// let data = vec![magic_number]; /// let content = create_binary_content(&data, true); /// /// assert_eq!( /// mofile(content), /// Err(IOError::IncorrectMagicNumber { /// magic_number_le: magic_number, /// magic_number_be: 537067520 /// }) /// ); /// ``` #[derive(Debug, PartialEq, Snafu)] pub enum IOError { /// An error has been happened trying to read the four bytes /// that should contain the unsigned 32 bits integer magic /// number. /// /// This mainly happens when your file has 0, 1, 2 or 3 bits /// long. Usually means that you have tried to read an empty /// file as the magic number are the four first bytes of MO files. #[snafu(display("Invalid mo file, error reading magic number"))] ErrorReadingMagicNumber {}, /// The magic number read from the MO file is not a valid one. /// /// The valid numbers are `0x950412de` (little endian) /// and `0xde120495` (big endian). If you are getting this /// error means that the first 4 bytes of your mo file /// converted as an unsigned 32 bits integer are not one /// of those numbers. /// /// Usually this happens because the buffer offset reading the /// file is not correct or the file has been saved with an /// incorrect magic number. #[snafu(display( concat!( "Invalid mo file, magic number is incorrect", " ({{magic_number_le}} read as le, {{magic_number_be}}", " read as be)", ) ))] IncorrectMagicNumber { magic_number_le: u32, magic_number_be: u32, }, /// The revision number of the MO file is not supported /// /// From the beginning, MO files have maintained the specification /// without changes. This number was introduced to make possible /// changes without breaking old compatibility, but never has been /// used. However, the library expects that should be 0 or 1 because /// the specification says that only those values are valid. /// /// This usually happens when the file data is corrupted as probably /// no MO files has been created with a revision number different /// than 0 or 1 ever. #[snafu(display("Invalid mo file, expected revision number 0 or 1, found {version}"))] UnsupportedMORevisionNumber { version: u32 }, /// Some of the data in the MO file is corrupted or malformed. /// /// This error happens when trying to read the data of the translations /// tables. It contains a different error message in each step of the /// parsing process. /// /// It means that data is corrupted or malformed in some way. It can /// be produced by reading the file with a wrong offset or by reading /// a file that is not a mo file. #[snafu(display("Invalid mo file, malformed or corrupted data found when {context}"))] CorruptedMOData { context: String }, } /// Syntax errors generated when the PO parser can't parse some content. /// /// # Examples /// /// ## Unescaped double quote found /// /// ```rust /// use rspolib::{pofile, errors::{SyntaxError, MaybeFilename}}; /// /// let content = r#"# /// msgid "Hello" /// msgstr "Ho"la" ///"#; /// /// assert_eq!( /// pofile(content), /// Err(SyntaxError::UnescapedDoubleQuoteFound { /// maybe_filename: MaybeFilename::new(content, false), /// line: 3, /// index: 11, /// }), /// ); /// ``` /// /// ## Unknown keyword /// /// ```rust /// use rspolib::{pofile, errors::{SyntaxError, MaybeFilename}}; /// /// let content = r#"# /// #| previous_message = "Good morning" /// msgid "Hello" /// msgstr "Hola" /// "#; /// /// assert_eq!( /// pofile(content), /// Err(SyntaxError::Custom { /// maybe_filename: MaybeFilename::new(content, false), /// line: 2, /// index: 0, /// message: "unknown keyword previous_message".to_string(), /// }), /// ); #[derive(Debug, PartialEq, Snafu)] pub enum SyntaxError { /// An unescaped double quote has been found in a po field string /// /// Happens mainly when you edit the file manually and forget /// to escape the double quote characters. It can also happen when you /// are reading a file that has been saved without escaping the double /// quotes in po string fields. #[snafu(display("Syntax error found{maybe_filename} at line {line} (index {index}): unescaped double quote found"))] UnescapedDoubleQuoteFound { maybe_filename: MaybeFilename, line: usize, index: usize, }, /// A generic syntax error that includes a message about what was /// the error has been found parsing a po file /// /// This happens when the parser finds a syntax error that is a expected /// syntax error, so it includes information about the error in the `message` /// field #[snafu(display("Syntax error found{maybe_filename} at line {line} (index {index}): {message}"))] Custom { maybe_filename: MaybeFilename, line: usize, index: usize, message: String, }, /// A generic syntax error without information about the line or the index #[snafu(display( "Syntax error found{maybe_filename}: {message}" ))] BasicCustom { maybe_filename: MaybeFilename, message: String, }, /// A generic syntax error has been found parsing a po file /// /// Happens when the parser finds a syntax error that is not /// covered by any other error. /// /// It can happen when you are reading a file that has been saved /// with strange characters in field names like `msgid` or `msgstr`. #[snafu(display("Syntax error found{maybe_filename} at line {line} (index {index})"))] Generic { maybe_filename: MaybeFilename, line: usize, index: usize, }, /// Unknown parsing state #[snafu(display("Unknown state {state}"))] UnknownState { state: String }, } /// Escaping errors generated by escaping functions. /// /// These errors are not generated by the parser, so you don't /// need to worry about them if you are using the parser, only /// if you are using the escaping functions directly. #[derive(Debug, PartialEq, Snafu)] pub enum EscapingError { #[snafu(display( "escape sequence found at end of string '{text}'" ))] EscapeAtEndOfString { text: String }, #[snafu(display( "invalid escaped character '{character}' found in '{text}'" ))] InvalidEscapedCharacter { text: String, character: char }, } rspolib-0.1.1/src/escaping.rs000064400000000000000000000075421046102023000142330ustar 00000000000000use crate::errors::EscapingError; use std::borrow::Cow; /// Escape characters in a PO string field pub fn escape(text: &str) -> Cow<'_, str> { let mut ret: String = String::with_capacity(text.len()); for char in text.chars() { match char { '"' => ret.push_str(r#"\""#), '\n' => ret.push_str(r#"\n"#), '\r' => ret.push_str(r#"\r"#), '\t' => ret.push_str(r#"\t"#), '\u{11}' => ret.push_str(r#"\v"#), '\u{8}' => ret.push_str(r#"\b"#), '\u{12}' => ret.push_str(r#"\f"#), '\\' => ret.push_str(r#"\\"#), c => ret.push(c), } } ret.into() } struct EscapedStringInterpreter<'a> { characters: std::str::Chars<'a>, } #[allow(clippy::needless_lifetimes)] impl<'a> Iterator for EscapedStringInterpreter<'a> { type Item = Result; fn next(&mut self) -> Option { self.characters.next().map(|c| match c { '\\' => match self.characters.next() { None => Err(EscapingError::EscapeAtEndOfString { text: self.characters.as_str().to_string(), }), Some('"') => Ok('"'), Some('n') => Ok('\n'), Some('r') => Ok('\r'), Some('t') => Ok('\t'), Some('b') => Ok('\u{8}'), Some('v') => Ok('\u{11}'), Some('f') => Ok('\u{12}'), Some('\\') => Ok('\\'), Some(c) => { Err(EscapingError::InvalidEscapedCharacter { text: self.characters.as_str().to_string(), character: c, }) } }, c => Ok(c), }) } } /// Unescape characters in a PO string field pub fn unescape(text: &str) -> Result, EscapingError> { if text.contains('\\') { (EscapedStringInterpreter { characters: text.chars(), }) .collect() } else { Ok(text.into()) } } #[cfg(test)] mod tests { use super::*; use std::collections::HashMap; const ESCAPES_EXPECTED: (&str, &str) = ( r#"foo \ \\ \t \r bar \n \v \b \f " baz"#, r#"foo \\ \\\\ \\t \\r bar \\n \\v \\b \\f \" baz"#, ); #[test] fn test_escape() { let escapes_map: HashMap = HashMap::from([ (r#"\"#.to_string(), r#"\\"#), (r#"\t"#.to_string(), r#"\\t"#), (r#"\r"#.to_string(), r#"\\r"#), ("\n".to_string(), "\\n"), (r"\n".to_string(), "\\\\n"), (r#"\v"#.to_string(), r#"\\v"#), (r#"\b"#.to_string(), r#"\\b"#), (r#"\f"#.to_string(), r#"\\f"#), (r#"""#.to_string(), r#"\""#), ]); for (value, expected) in escapes_map { assert_eq!(escape(&value), expected); } } #[test] fn test_escape_all() { let (escapes, expected) = ESCAPES_EXPECTED; assert_eq!(escape(escapes), expected); } #[test] fn test_unescape() -> Result<(), EscapingError> { let escapes_map: HashMap = HashMap::from([ (r#"\\"#.to_string(), r#"\"#), (r#"\\n"#.to_string(), r#"\n"#), (r#"\\t"#.to_string(), r#"\t"#), (r#"\\r"#.to_string(), r#"\r"#), (r#"\""#.to_string(), r#"""#), (r#"\\v"#.to_string(), r#"\v"#), (r#"\\b"#.to_string(), r#"\b"#), (r#"\\f"#.to_string(), r#"\f"#), ]); for (value, expected) in escapes_map { assert_eq!(unescape(&value)?, expected); } Ok(()) } #[test] fn test_unescape_all() -> Result<(), EscapingError> { let (expected, escapes) = ESCAPES_EXPECTED; assert_eq!(unescape(escapes)?, expected); Ok(()) } } rspolib-0.1.1/src/file/mod.rs000064400000000000000000000146731046102023000141430ustar 00000000000000pub mod mofile; pub mod pofile; use std::borrow::Cow; use std::collections::HashMap; use std::fmt; use std::fs::File; use std::io::Write; use std::path::Path; use natord::compare as compare_natural_order; const METADATA_KEYS_ORDER: [&str; 11] = [ "Project-Id-Version", "Report-Msgid-Bugs-To", "POT-Creation-Date", "PO-Revision-Date", "Last-Translator", "Language-Team", "Language", "MIME-Version", "Content-Type", "Content-Transfer-Encoding", "Plural-Forms", ]; /// Save file as a PO file with the `save_as_pofile` method pub trait SaveAsPOFile { /// Save the file as a PO file to the given path fn save_as_pofile(&self, path: &str) where Self: fmt::Display, { let mut file = File::create(path).unwrap(); file.write_all(self.to_string().as_bytes()).ok(); } } /// Save file with the `save` method pub trait Save { /// Save the file to the given path fn save(&self, path: &str); } /// Save file as a MO file with the `save_as_mofile` method pub trait SaveAsMOFile { /// Save the file as a MO file to the given path fn save_as_mofile(&self, path: &str); } /// Provides functions to convert to MO files content as bytes /// /// * `as_bytes` method as an alias to `as_bytes_le`. /// * `as_bytes_le` method to return the content as bytes in /// little endian byte order. /// * `as_bytes_be` method to return the content as bytes in /// big endian byte order. pub trait AsBytes { /// Return the content as bytes fn as_bytes(&self) -> Cow<[u8]>; /// Return the content as bytes in little endian encoding fn as_bytes_le(&self) -> Cow<[u8]>; /// Return the content as bytes in big endian encoding fn as_bytes_be(&self) -> Cow<[u8]>; } /// File options struct passed when creating a new PO or MO file /// /// # Examples /// /// ```rust /// use std::fs; /// use rspolib::FileOptions; /// /// // From path /// let opts = FileOptions::from("tests-data/all.po"); /// assert_eq!(opts.path_or_content, "tests-data/all.po"); /// assert_eq!(opts.wrapwidth, 78); /// /// // From path and wrap width /// let opts = FileOptions::from(("tests-data/obsoletes.po", 80)); /// assert_eq!(opts.path_or_content, "tests-data/obsoletes.po"); /// assert_eq!(opts.wrapwidth, 80); /// /// // From bytes /// let bytes = fs::read("tests-data/obsoletes.po").unwrap(); /// let opts = FileOptions::from(bytes); /// ``` #[derive(Clone, Debug, PartialEq)] pub struct FileOptions { /// Path or content to the file pub path_or_content: String, /// Wrap width for the PO file, used when converted as a string pub wrapwidth: usize, /// Content as bytes, used by MO files when the content is passed as bytes pub byte_content: Option>, } impl Default for FileOptions { fn default() -> Self { Self { path_or_content: "".to_string(), wrapwidth: 78, byte_content: None, } } } impl From<&FileOptions> for FileOptions { fn from(options: &Self) -> Self { Self { path_or_content: options.path_or_content.clone(), wrapwidth: options.wrapwidth, ..Default::default() } } } impl<'a> From<&'a str> for FileOptions { fn from(path_or_content: &'a str) -> Self { Self { path_or_content: path_or_content.to_string(), ..Default::default() } } } impl<'a> From<(&'a str, usize)> for FileOptions { fn from(opts: (&'a str, usize)) -> Self { Self { path_or_content: opts.0.to_string(), wrapwidth: opts.1, ..Default::default() } } } impl From> for FileOptions { fn from(byte_content: Vec) -> Self { Self { byte_content: Some(byte_content), ..Default::default() } } } impl From<(Vec, usize)> for FileOptions { fn from((byte_content, wrapwidth): (Vec, usize)) -> Self { Self { path_or_content: "".to_string(), wrapwidth, byte_content: Some(byte_content), } } } impl From<&Path> for FileOptions { fn from(path: &Path) -> Self { Self { path_or_content: path.to_str().unwrap().to_string(), ..Default::default() } } } fn metadata_hashmap_to_msgstr( metadata: &HashMap, ) -> String { let ordered_map = metadata_hashmap_to_ordered(metadata); let mut parts: Vec = Vec::with_capacity(ordered_map.len()); for (key, value) in ordered_map { let mut msgstr = String::with_capacity(key.len() + value.len() + 2); msgstr.push_str(&key); msgstr.push_str(": "); msgstr.push_str(&value); parts.push(msgstr); } parts.join("\n") } fn metadata_hashmap_to_ordered( metadata: &HashMap, ) -> Vec<(String, String)> { let mut ret: Vec<(String, String)> = Vec::with_capacity(METADATA_KEYS_ORDER.len()); for key in METADATA_KEYS_ORDER { if metadata.contains_key(key) { let value = metadata.get(key).unwrap(); ret.push((key.to_string(), value.to_string())); } } let mut metadata_keys = metadata.keys().collect::>(); metadata_keys.sort_by(|&a, &b| compare_natural_order(a, b)); for key in metadata_keys { if !METADATA_KEYS_ORDER.contains(&key.as_str()) { let value = metadata.get(key).unwrap(); ret.push((key.to_string(), value.to_string())); } } ret } #[cfg(test)] mod tests { use super::*; #[test] fn options_from() { // FileOptions from &FileOptions let options = FileOptions { wrapwidth: 50, path_or_content: "foobar".to_string(), byte_content: None, }; let options_from_options = FileOptions::from(&options); assert_eq!(options_from_options.wrapwidth, 50); assert_eq!(options_from_options.path_or_content, "foobar"); // FileOptions from &str let options_from_str = FileOptions::from("foobar"); assert_eq!(options_from_str.wrapwidth, 78); assert_eq!(options_from_str.path_or_content, "foobar"); // FileOptions from (&str, usize) let options_from_str_and_usize = FileOptions::from(("foobar", 50)); assert_eq!(options_from_str_and_usize.wrapwidth, 50); assert_eq!( options_from_str_and_usize.path_or_content, "foobar" ); } } rspolib-0.1.1/src/file/mofile.rs000064400000000000000000000470751046102023000146410ustar 00000000000000use std::borrow::Cow; use std::collections::HashMap; use std::fmt; use std::fs::File; use std::io::Write; use std::path::Path; use crate::entry::{ mo_metadata_entry_to_string, MOEntry, MsgidEotMsgctxt, }; use crate::errors::IOError; use crate::file::{ metadata_hashmap_to_msgstr, pofile::POFile, AsBytes, FileOptions, Save, SaveAsMOFile, SaveAsPOFile, }; use crate::moparser::{MOFileParser, MAGIC, MAGIC_SWAPPED}; fn empty_msgctxt_predicate(_: &MOEntry, _: &str) -> bool { true } fn msgctxt_predicate(entry: &MOEntry, msgctxt: &str) -> bool { entry.msgctxt.as_ref().unwrap_or(&"".to_string()) == msgctxt } fn by_msgid_predicate(entry: &MOEntry, value: &str) -> bool { entry.msgid == value } fn by_msgstr_predicate(entry: &MOEntry, value: &str) -> bool { entry.msgstr.as_ref().unwrap_or(&"".to_string()) == value } fn by_msgctxt_predicate(entry: &MOEntry, value: &str) -> bool { entry.msgctxt.as_ref().unwrap_or(&"".to_string()) == value } fn by_msgid_plural_predicate(entry: &MOEntry, value: &str) -> bool { entry.msgid_plural.as_ref().unwrap_or(&"".to_string()) == value } /// MO files factory function /// /// Read a MO file from a path, parse from content as bytes or /// from a [FileOptions] struct. /// /// # Examples /// /// ## Read a MO file from a path /// /// ```rust /// use rspolib::mofile; /// /// let file = mofile("tests-data/all.mo").unwrap(); /// assert_eq!(file.entries.len(), 7); /// ``` /// /// ## Read a MO file from bytes /// /// ```rust /// use rspolib::mofile; /// /// let bytes = std::fs::read("tests-data/all.mo").unwrap(); /// let file = mofile(bytes).unwrap(); /// assert_eq!(file.entries.len(), 7); /// ``` pub fn mofile(options: Opt) -> Result where Opt: Into, { let mut parser = MOFileParser::new(options.into()); parser.parse()?; Ok(parser.file) } /// MO file #[derive(Clone, Debug, PartialEq)] pub struct MOFile { /// Magic number, either [MAGIC] or [MAGIC_SWAPPED] pub magic_number: Option, /// Version number, either 0 or 1 pub version: Option, /// Metadata as a hash map pub metadata: HashMap, /// Message entries pub entries: Vec, /// File options. See [FileOptions]. pub options: FileOptions, } impl MOFile { pub fn new(options: FileOptions) -> Self { Self { options, magic_number: None, version: None, metadata: HashMap::new(), entries: Vec::new(), } } /// Returns the metadata as a [MOEntry] pub fn metadata_as_entry(&self) -> MOEntry { let mut entry = MOEntry::new("".to_string(), None, None, vec![], None); if !self.metadata.is_empty() { entry.msgstr = Some(metadata_hashmap_to_msgstr(&self.metadata)) } entry } /// Find entries by a given field and value /// /// The field defined in the `by` argument can be one of: /// /// * `msgid` /// * `msgstr` /// * `msgctxt` /// * `msgid_plural` /// /// Passing the optional `msgctxt` argument the entry /// will also must match with the given context. pub fn find( &self, value: &str, by: &str, msgctxt: Option<&str>, ) -> Vec<&MOEntry> { let mut entries: Vec<&MOEntry> = Vec::new(); let msgctxt_predicate: &dyn Fn(&MOEntry, &str) -> bool = match msgctxt { Some(_) => &msgctxt_predicate, None => &empty_msgctxt_predicate, }; let by_predicate: &dyn Fn(&MOEntry, &str) -> bool = match by { "msgid" => &by_msgid_predicate, "msgstr" => &by_msgstr_predicate, "msgctxt" => &by_msgctxt_predicate, "msgid_plural" => &by_msgid_plural_predicate, _ => &|_: &MOEntry, _: &str| false, }; for entry in &self.entries { if by_predicate(entry, value) && msgctxt_predicate(entry, msgctxt.unwrap_or("")) { entries.push(entry); } } entries } /// Find an entry by msgid pub fn find_by_msgid(&self, msgid: &str) -> Option<&MOEntry> { self.entries.iter().find(|e| e.msgid == msgid) } /// Find an entry by msgid and msgctxt pub fn find_by_msgid_msgctxt( &self, msgid: &str, msgctxt: &str, ) -> Option<&MOEntry> { self.entries.iter().find(|e| { e.msgid == msgid && e.msgctxt == Some(msgctxt.to_string()) }) } /// Remove an entry from the file pub fn remove(&mut self, entry: &MOEntry) { self.entries.retain(|e| e != entry); } /// Remove the first entry that has the same msgid pub fn remove_by_msgid(&mut self, msgid: &str) { self.entries.retain(|e| e.msgid != msgid); } /// Remove the first entry that has the same msgid and msgctxt pub fn remove_by_msgid_msgctxt( &mut self, msgid: &str, msgctxt: &str, ) { self.entries.retain(|e| { e.msgid != msgid || e.msgctxt.as_ref().unwrap_or(&"".to_string()) != msgctxt }); } /// Returns the entry as a bytes vector /// /// Specify the magic number and the revision number /// of the generated MO version of the file. /// /// This method does not check the validity of the values /// `magic_number` and `revision_version` to allow the /// experimental developing of other revision of MO files, /// so be careful about the passed values if you use it. /// /// Valid values for the magic number are [MAGIC] and [MAGIC_SWAPPED]. /// Valid values for the revision number are 0 and 1. /// /// # Example /// /// ```rust /// use rspolib::mofile; /// /// let file = mofile("tests-data/all.mo").unwrap(); /// let bytes = file.as_bytes_with(rspolib::MAGIC_SWAPPED, 1); /// assert_eq!(bytes.len(), 1327); /// ``` pub fn as_bytes_with( &self, magic_number: u32, revision_number: u32, ) -> Cow<[u8]> { let metadata_entry = self.metadata_as_entry(); // Select byte order based on magic number let bytes_reader: fn(u32) -> [u8; 4] = match magic_number { MAGIC_SWAPPED => u32::to_be_bytes, _ => u32::to_le_bytes, }; let mut entries: Vec<&MOEntry> = vec![&metadata_entry]; entries.extend(&self.entries); entries.sort_unstable_by(|a, b| { a.msgid_eot_msgctxt().cmp(&b.msgid_eot_msgctxt()) }); let entries_length = entries.len(); let mut offsets: Vec<(usize, usize, usize, usize)> = vec![]; let mut ids = "".to_string(); let mut strs = "".to_string(); for e in entries { // For each string, we need size and file offset. Each // string is NUL terminated but the NUL does not count // into the size. let mut msgid = "".to_string(); let mut msgstr = "".to_string(); if let Some(msgctxt) = &e.msgctxt { msgid.push_str(msgctxt); msgid.push('\u{4}'); } if let Some(msgid_plural) = &e.msgid_plural { // handle msgid_plural msgid.push_str(&e.msgid); msgid.push('\u{0}'); msgid.push_str(msgid_plural); // handle msgstr_plural let msgstr_plural_length = &e.msgstr_plural.len(); for (i, v) in e.msgstr_plural.iter().enumerate() { msgstr.push_str(v); if i < msgstr_plural_length - 1 { msgstr.push('\u{0}'); } } } else { msgid.push_str(&e.msgid); if let Some(m) = &e.msgstr { msgstr.push_str(m); } } offsets.push(( ids.len(), msgid.len(), strs.len(), msgstr.len(), )); ids.push_str(&msgid); ids.push('\u{0}'); strs.push_str(&msgstr); strs.push('\u{0}'); } // The header is 7 32-bit unsigned integers. let keystart = 7 * 4 + 16 * entries_length; // and the values start after the keys let valuestart = keystart + ids.len(); // The string table first has the list of keys, then the list of values. // Each entry has first the size of the string, then the file offset. let mut koffsets: Vec<(usize, usize)> = vec![]; let mut voffsets: Vec<(usize, usize)> = vec![]; for (o1, l1, o2, l2) in offsets { koffsets.push((l1, o1 + keystart)); voffsets.push((l2, o2 + valuestart)); } let mut final_offsets: Vec = vec![]; for (l, o) in koffsets { final_offsets.extend(bytes_reader(l as u32)); final_offsets.extend(bytes_reader(o as u32)); } for (l, o) in voffsets { final_offsets.extend(bytes_reader(l as u32)); final_offsets.extend(bytes_reader(o as u32)); } let mut output: Vec = Vec::with_capacity( 7 * 4 + 8 * entries_length + ids.len() + strs.len(), ); // magic number output.extend(bytes_reader(MAGIC)); // version output.extend(bytes_reader(revision_number)); // number of entries output.extend(bytes_reader(entries_length as u32)); // start of key index output.extend(bytes_reader(7 * 4)); // start of value index output.extend(bytes_reader( 7 * 4 + (entries_length as u32) * 8, )); // size and offset of hash table, we don't use hash tables output.extend([0, 0, 0, 0]); output.extend(bytes_reader(keystart as u32)); output.extend(final_offsets); output.extend(ids.as_bytes()); output.extend(strs.as_bytes()); output.into() } } impl fmt::Display for MOFile { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut ret = String::from("#\n"); ret.push_str(&mo_metadata_entry_to_string( &self.metadata_as_entry(), )); ret.push('\n'); for entry in &self.entries { ret.push_str(&entry.to_string()); ret.push('\n'); } ret.remove(ret.len() - 1); write!(f, "{ret}") } } // the method save_as_pofile is implemented by the trait impl SaveAsPOFile for MOFile {} impl Save for MOFile { /// Save the MOFile to a file at the given path fn save(&self, path: &str) { let mut file = File::create(path).unwrap(); file.write_all(&self.as_bytes()).ok(); } } impl SaveAsMOFile for MOFile { /// Save the MOFile to a file at the given path fn save_as_mofile(&self, path: &str) { self.save(path); } } impl AsBytes for MOFile { /// Return the MOFile as a vector of bytes in little endian fn as_bytes(&self) -> Cow<[u8]> { self.as_bytes_with(MAGIC, 0) } /// Return the MOFile as a vector of bytes in little endian fn as_bytes_le(&self) -> Cow<[u8]> { self.as_bytes_with(MAGIC, 0) } /// Return the MOFile as a vector of bytes in big endian fn as_bytes_be(&self) -> Cow<[u8]> { self.as_bytes_with(MAGIC_SWAPPED, 0) } } impl From<&POFile> for MOFile { fn from(file: &POFile) -> MOFile { let mut new_file = MOFile::new(file.options.clone()); new_file.metadata = file.metadata.clone(); new_file.entries = file .translated_entries() .iter() .map(|e| MOEntry::from(*e)) .collect(); new_file } } impl From> for MOFile { fn from(entries: Vec<&MOEntry>) -> Self { let mut file = MOFile::new("".into()); for entry in entries { file.entries.push(entry.clone()); } file } } impl From<&Path> for MOFile { fn from(path: &Path) -> Self { MOFile::new(path.to_str().unwrap().into()) } } #[cfg(test)] mod tests { use super::*; use crate::pofile; use std::fs; use std::io::Read; use std::path::Path; use unicode_width::UnicodeWidthStr; #[test] fn mofile_test() { let path = "tests-data/all.mo"; let file = mofile(path).unwrap(); assert_eq!(file.entries.len(), 7); } #[test] fn mofile_metadata_as_entry() { // File with metadata let path = "tests-data/all.mo"; let file = mofile(path).unwrap(); let entry = file.metadata_as_entry(); let msgstr = entry.msgstr.unwrap(); assert_eq!(entry.msgid, ""); assert_eq!(msgstr.lines().count(), 10); // File without metadata let path = "tests-data/empty-metadata.mo"; let file = mofile(path).unwrap(); let entry = file.metadata_as_entry(); assert_eq!(entry.msgid, ""); assert_eq!(entry.msgstr.is_none(), true); } #[test] fn mofile_from_pofile() { let path = "tests-data/all.po"; let po_file = pofile(path).unwrap(); let mo_file = MOFile::from(&po_file); assert_eq!( mo_file.entries.len(), po_file.translated_entries().len(), ); assert_eq!(mo_file.metadata.len(), po_file.metadata.len()); } #[test] fn mofile_from_std_path() { let file = MOFile::from(Path::new("tests-data/all.mo")); assert_eq!(file.options.path_or_content, "tests-data/all.mo"); } #[test] fn mofile_to_string() { let mo_path = "tests-data/all.mo"; let file = mofile(mo_path).unwrap(); let file_as_string = file.to_string(); for line in file_as_string.lines() { let width = UnicodeWidthStr::width(line); assert!(width <= file.options.wrapwidth + 2); } } #[test] fn mofile_as_bytes() { // generated by msgfmt let path = "tests-data/all.mo"; let file = mofile(path).unwrap(); let file_as_bytes = file.as_bytes(); // generated by polib let polib_path = "tests-data/all-polib.mo"; let polib_file = mofile(polib_path).unwrap(); let polib_file_as_bytes = polib_file.as_bytes(); // The same number of bytes assert_eq!(file_as_bytes.len(), polib_file_as_bytes.len()); // and the same bytes for (rspolib_byte, polib_byte) in file_as_bytes.iter().zip(polib_file_as_bytes.iter()) { assert_eq!(rspolib_byte, polib_byte); } // the implementation differs from msgfmt let buffer: Vec = fs::read(path).unwrap(); assert_ne!(file_as_bytes, buffer); assert_ne!(polib_file_as_bytes, buffer); // msgfmt generates more bytes assert!(file_as_bytes.len() < buffer.len()); assert!(polib_file_as_bytes.len() < buffer.len()); } #[test] fn mofile_save_as_pofile() { let tmpdir = "tests-data/tests"; let path = "tests-data/all.mo"; let file = mofile(path).unwrap(); let file_as_string = file.to_string(); let tmp_path = Path::new(&tmpdir).join("all.po"); let tmp_path_str = tmp_path.to_str().unwrap(); file.save_as_pofile(tmp_path_str); assert_eq!( file_as_string, fs::read_to_string(tmp_path_str).unwrap() ); fs::remove_file(tmp_path_str).unwrap(); } fn mofile_save_test( basename: &str, read_bytes_from_file: bool, save_method_name: &str, ) { let tmpdir = "tests-data/tests"; let path = "tests-data/all.mo"; let file = mofile(path).unwrap(); let tmp_path = Path::new(&tmpdir).join(format!("{}.mo", basename)); let tmp_path_str = tmp_path.to_str().unwrap(); if save_method_name == "save" { file.save(tmp_path_str); } else { file.save_as_mofile(tmp_path_str); } // exists assert!(tmp_path.is_file()); let file_bytes = match read_bytes_from_file { true => fs::read(tmp_path_str).unwrap(), false => file.as_bytes().into_owned(), }; let mut file_bytes = file_bytes.as_slice(); let mut buf: [u8; 4] = [0, 0, 0, 0]; // has correct magic number file_bytes.read_exact(&mut buf).unwrap(); let magic_number = u32::from_le_bytes(buf); assert_eq!(magic_number, MAGIC); // has correct revision number file_bytes.read_exact(&mut buf).unwrap(); let revision_number = u32::from_le_bytes(buf); assert_eq!(revision_number, 0); // has correct number of entries file_bytes.read_exact(&mut buf).unwrap(); let number_of_entries = u32::from_le_bytes(buf); assert_eq!( number_of_entries, // +1 here because includes the header entry file.entries.len() as u32 + 1, ); } #[test] fn mofile_save_as_mofile() { mofile_save_test( "mofile_save_as_mofile-file", true, "save_as_mofile", ); mofile_save_test( "mofile_save_as_mofile-struct", false, "save_as_mofile", ); } #[test] fn mofile_save() { mofile_save_test("mofile_save-file", true, "save"); mofile_save_test("mofile_save-struct", false, "save"); } #[test] fn remove() { let mut entry_1 = MOEntry::from("msgid 1"); entry_1.msgstr = Some("msgstr 1".to_string()); let mut entry_2 = MOEntry::from("msgid 2"); entry_2.msgstr = Some("msgstr 2".to_string()); let mut file = MOFile::from(vec![&entry_1, &entry_2]); assert_eq!(file.entries.len(), 2); // remove by entry file.remove(&entry_1); assert_eq!(file.entries.len(), 1); assert_eq!(file.entries[0].msgid, "msgid 2"); file.entries.push(entry_1); assert_eq!(file.entries.len(), 2); // remove by msgid file.remove_by_msgid("msgid 2"); assert_eq!(file.entries.len(), 1); assert_eq!(file.entries[0].msgid, "msgid 1"); // remove by msgid and msgctxt entry_2.msgctxt = Some("msgctxt 2".to_string()); entry_2.msgid = "msgid 1".to_string(); file.entries.push(entry_2); assert_eq!(file.entries.len(), 2); file.remove_by_msgid_msgctxt("msgid 1", "msgctxt 2"); assert_eq!(file.entries.len(), 1); assert_eq!(file.entries[0].msgid, "msgid 1"); assert_eq!( file.entries[0].msgstr.as_ref().unwrap(), "msgstr 1", ); } #[test] fn find() { let mut entry_1 = MOEntry::from("msgid 1"); entry_1.msgstr = Some("msgstr 1".to_string()); let mut entry_2 = MOEntry::from("msgid 2"); entry_2.msgstr = Some("msgstr 2".to_string()); let mut file = MOFile::from(vec![&entry_1, &entry_2]); assert_eq!(file.entries.len(), 2); // find by msgid assert_eq!( file.find_by_msgid("msgid 2").unwrap().msgid, "msgid 2" ); // find by msgid and msgctxt entry_2.msgctxt = Some("msgctxt 2".to_string()); entry_2.msgid = "msgid 1".to_string(); file.entries.push(entry_2); assert_eq!(file.entries.len(), 3); assert_eq!( file.find_by_msgid_msgctxt("msgid 1", "msgctxt 2") .unwrap() .msgstr .as_ref() .unwrap(), "msgstr 2", ); } } rspolib-0.1.1/src/file/pofile.rs000064400000000000000000000637761046102023000146520ustar 00000000000000use std::borrow::Cow; use std::collections::HashMap; use std::fmt; use std::path::Path; use crate::entry::{ po_metadata_entry_to_string, POEntry, Translated, }; use crate::errors::SyntaxError; use crate::file::{ metadata_hashmap_to_msgstr, mofile::MOFile, AsBytes, FileOptions, Save, SaveAsMOFile, SaveAsPOFile, }; use crate::moparser::{MAGIC, MAGIC_SWAPPED}; use crate::poparser::POFileParser; use crate::traits::Merge; fn empty_msgctxt_predicate(_: &POEntry, _: &str) -> bool { true } fn msgctxt_predicate(entry: &POEntry, msgctxt: &str) -> bool { entry.msgctxt.as_ref().unwrap_or(&"".to_string()) == msgctxt } fn by_msgid_predicate(entry: &POEntry, value: &str) -> bool { entry.msgid == value } fn by_msgstr_predicate(entry: &POEntry, value: &str) -> bool { entry.msgstr.as_ref().unwrap_or(&"".to_string()) == value } fn by_msgctxt_predicate(entry: &POEntry, value: &str) -> bool { entry.msgctxt.as_ref().unwrap_or(&"".to_string()) == value } fn by_msgid_plural_predicate(entry: &POEntry, value: &str) -> bool { entry.msgid_plural.as_ref().unwrap_or(&"".to_string()) == value } fn by_previous_msgid_predicate(entry: &POEntry, value: &str) -> bool { entry.previous_msgid.as_ref().unwrap_or(&"".to_string()) == value } fn by_previous_msgid_plural_predicate( entry: &POEntry, value: &str, ) -> bool { entry .previous_msgid_plural .as_ref() .unwrap_or(&"".to_string()) == value } fn by_previous_msgctxt_predicate( entry: &POEntry, value: &str, ) -> bool { entry.previous_msgctxt.as_ref().unwrap_or(&"".to_string()) == value } /// PO files factory function. /// /// It takes an argument that could be either: /// /// * A string as path to an existent file. /// * The content of a PO file as string. /// * The content of a PO file as bytes. /// * A [FileOptions] struct. /// /// # Examples /// /// ## Open from path /// /// ```rust /// use rspolib::pofile; /// /// let file = pofile("tests-data/obsoletes.po").unwrap(); /// ``` /// /// ## Open from content /// /// ```rust /// use rspolib::pofile; /// /// let content = r#"# /// msgid "" /// msgstr "" /// /// msgid "A message" /// msgstr "Un mensaje" /// "#; /// /// let file = pofile(content).unwrap(); /// ``` /// /// ## Open from bytes /// /// ```rust /// use std::fs; /// use rspolib::pofile; /// /// let bytes_content = fs::read("tests-data/all.po").unwrap(); /// let file = pofile(bytes_content).unwrap(); /// ``` /// /// ## Tuples into [FileOptions] /// /// ```rust /// use rspolib::pofile; /// /// // Wrap width /// let file = pofile(("tests-data/all.po", 75)).unwrap(); /// ``` /// /// ## Explicitly from [FileOptions] /// /// ```rust /// use std::fs; /// use rspolib::{pofile, FileOptions as POFileOptions}; /// /// let file = pofile(POFileOptions::default()).unwrap(); /// /// // Path or content /// let opts = POFileOptions::from("tests-data/obsoletes.po"); /// let file = pofile(opts).unwrap(); /// /// // Wrap width /// let opts = POFileOptions::from(("tests-data/all.po", 75)); /// let file = pofile(opts).unwrap(); /// ``` pub fn pofile(options: Opt) -> Result where Opt: Into, { let mut parser = POFileParser::new(options.into()); parser.parse()?; Ok(parser.file) } /// PO file #[derive(Clone, Debug, PartialEq)] pub struct POFile { /// Entries of the file. pub entries: Vec, /// Header of the file, if any. Optionally defined /// in PO files before the first entry. pub header: Option, /// First optional field of PO files that describes /// the metadata of the file stored as a hash map. pub metadata: HashMap, /// Whether the metadata is marked with the `fuzzy` /// flag or not. pub metadata_is_fuzzy: bool, /// Options defined for the file. See [FileOptions]. pub options: FileOptions, } impl POFile { pub fn new(options: FileOptions) -> Self { Self { options, header: None, metadata: HashMap::new(), metadata_is_fuzzy: false, entries: Vec::new(), } } /// Remove an entry from the file pub fn remove(&mut self, entry: &POEntry) { self.entries.retain(|e| e != entry); } /// Remove the first entry that has the same msgid pub fn remove_by_msgid(&mut self, msgid: &str) { self.entries.retain(|e| e.msgid != msgid); } /// Remove the first entry that has the same msgid and msgctxt pub fn remove_by_msgid_msgctxt( &mut self, msgid: &str, msgctxt: &str, ) { self.entries.retain(|e| { e.msgid != msgid || e.msgctxt.as_ref().unwrap_or(&"".to_string()) != msgctxt }); } /// Find entries by a given field and value /// /// The field defined in the `by` argument can be one of: /// /// * `msgid` /// * `msgstr` /// * `msgctxt` /// * `msgid_plural` /// * `previous_msgid` /// * `previous_msgid_plural` /// * `previous_msgctxt` /// /// Passing the optional `msgctxt` argument the entry /// will also must match with the given context. /// /// If `include_obsolete_entries` is set to `true` the /// search will include obsolete entries. pub fn find( &self, value: &str, by: &str, msgctxt: Option<&str>, include_obsolete_entries: bool, ) -> Vec<&POEntry> { let mut entries: Vec<&POEntry> = Vec::new(); let msgctxt_predicate: &dyn Fn(&POEntry, &str) -> bool = match msgctxt { Some(_) => &msgctxt_predicate, None => &empty_msgctxt_predicate, }; let by_predicate: &dyn Fn(&POEntry, &str) -> bool = match by { "msgid" => &by_msgid_predicate, "msgstr" => &by_msgstr_predicate, "msgctxt" => &by_msgctxt_predicate, "msgid_plural" => &by_msgid_plural_predicate, "previous_msgid" => &by_previous_msgid_predicate, "previous_msgid_plural" => { &by_previous_msgid_plural_predicate } "previous_msgctxt" => &by_previous_msgctxt_predicate, _ => &|_: &POEntry, _: &str| false, }; for entry in &self.entries { if !include_obsolete_entries && entry.obsolete { continue; } if by_predicate(entry, value) && msgctxt_predicate(entry, msgctxt.unwrap_or("")) { entries.push(entry); } } entries } /// Find an entry by his msgid pub fn find_by_msgid(&self, msgid: &str) -> Option { self.entries.iter().find(|e| e.msgid == msgid).cloned() } /// Find an entry by msgid and msgctxt pub fn find_by_msgid_msgctxt( &self, msgid: &str, msgctxt: &str, ) -> Option { self.entries .iter() .find(|e| { e.msgid == msgid && e.msgctxt.as_ref().unwrap_or(&"".to_string()) == msgctxt }) .cloned() } /// Returns the percent of the entries translated in the file pub fn percent_translated(&self) -> f32 { let translated = self.translated_entries().len(); let total = self.entries.len(); if total == 0 { 0.0 } else { (translated as f32 / total as f32) * 100.0 } } /// Returns references to the translated entries of the file pub fn translated_entries(&self) -> Vec<&POEntry> { let mut entries: Vec<&POEntry> = Vec::new(); for entry in &self.entries { if entry.translated() { entries.push(entry); } } entries } /// Returns references to the untranslated entries of the file pub fn untranslated_entries(&self) -> Vec<&POEntry> { let mut entries: Vec<&POEntry> = Vec::new(); for entry in &self.entries { if !entry.translated() { entries.push(entry); } } entries } /// Returns references to the obsolete entries of the file pub fn obsolete_entries(&self) -> Vec<&POEntry> { let mut entries: Vec<&POEntry> = Vec::new(); for entry in &self.entries { if entry.obsolete { entries.push(entry); } } entries } /// Returns references to the fuzzy entries of the file pub fn fuzzy_entries(&self) -> Vec<&POEntry> { let mut entries: Vec<&POEntry> = Vec::new(); for entry in &self.entries { if entry.fuzzy() && !entry.obsolete { entries.push(entry); } } entries } /// Returns the metadata of the file as an entry. /// /// This method is not really useful because the /// ``to_string()`` version will not be guaranteed to be /// correct. /// /// If you want to manipulate the metadata, change /// the content of the field `metadata` in the file. /// /// If you still want to render a metadata entry as /// a string, use the function [po_metadata_entry_to_string]: /// /// ```rust /// use rspolib::{ /// pofile, /// po_metadata_entry_to_string, /// }; /// /// let file = pofile("tests-data/metadata.po").unwrap(); /// let entry = file.metadata_as_entry(); /// let entry_str = po_metadata_entry_to_string(&entry, true); /// assert!(entry_str.starts_with("#, fuzzy\nmsgid \"\"")); /// ``` pub fn metadata_as_entry(&self) -> POEntry { let mut entry = POEntry::new(0); if self.metadata_is_fuzzy { entry.flags.push("fuzzy".to_string()); } if !self.metadata.is_empty() { entry.msgstr = Some(metadata_hashmap_to_msgstr(&self.metadata)) } entry } } impl fmt::Display for POFile { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut ret: String = match self.header { Some(ref header) => { if header.is_empty() { "#\n".to_string() } else { let mut header_repr = String::new(); for line in header.lines() { if line.is_empty() { header_repr.push_str("#\n"); } else { header_repr.reserve(line.len() + 3); header_repr.push_str("# "); header_repr.push_str(line); header_repr.push('\n'); } } header_repr } } None => "#\n".to_string(), }; // Metadata should not include spaces after values ret.push_str(&po_metadata_entry_to_string( &self.metadata_as_entry(), self.metadata_is_fuzzy, )); ret.push('\n'); let mut entries_ret = String::new(); let mut obsolete_entries_ret = String::new(); for entry in &self.entries { if entry.obsolete { obsolete_entries_ret.push_str(&entry.to_string()); obsolete_entries_ret.push('\n'); } else { entries_ret.push_str(&entry.to_string()); entries_ret.push('\n'); } } ret.push_str(&entries_ret); ret.push_str(&obsolete_entries_ret); ret.pop(); write!(f, "{ret}") } } // Method `save_as_pofile` is implemented in the trait impl SaveAsPOFile for POFile {} impl Save for POFile { /// Save the PO file as the given path fn save(&self, path: &str) { self.save_as_pofile(path); } } impl SaveAsMOFile for POFile { /// Save the PO file as a MO file as the given path fn save_as_mofile(&self, path: &str) { MOFile::from(self).save(path); } } impl<'a> From<&'a str> for POFile { fn from(path_or_content: &'a str) -> Self { pofile(path_or_content).unwrap() } } impl Merge for POFile { /// Merge another PO file into this one and return a new one /// /// Recursively calls `merge` on each entry if they are found /// in the current file searching by msgid and msgctxt. If not /// found, generates a new entry. /// /// This method is commonly used to merge a POT reference file /// with a PO file. fn merge(&mut self, other: POFile) { for other_entry in other.entries.as_slice() { let entry: Option = match other_entry.msgctxt { Some(ref msgctxt) => self.find_by_msgid_msgctxt( &other_entry.msgid, msgctxt, ), None => self.find_by_msgid(&other_entry.msgid), }; if let Some(e) = entry { let mut entry = e; entry.merge(other_entry.clone()); } else { let mut entry = POEntry::new(0); entry.merge(other_entry.clone()); self.entries.push(entry); } } let self_entries: &mut Vec = self.entries.as_mut(); for entry in self_entries { if other.find_by_msgid(&entry.msgid).is_none() { entry.obsolete = true; } } } } impl AsBytes for POFile { /// Return the PO file content as a bytes vector of the MO file version /// /// The MO file is encoded with little /// endian magic number and revision number 0 /// /// Use directly [MOFile::as_bytes_with] to customize /// the magic number and revision number: /// /// ```rust /// use rspolib::{pofile, MAGIC_SWAPPED, MOFile}; /// /// let file = pofile("tests-data/all.po").unwrap(); /// let bytes = MOFile::from(&file).as_bytes_with(MAGIC_SWAPPED, 1); /// ``` fn as_bytes(&self) -> Cow<[u8]> { let mofile = MOFile::from(self); let result = mofile.as_bytes_with(MAGIC, 0); Cow::Owned(result.into_owned()) } /// Return the PO file content as a bytes vector of the MO file version /// /// Just an alias for [POFile::as_bytes], for consistency with [MOFile]. fn as_bytes_le(&self) -> Cow<[u8]> { self.as_bytes() } /// Return the PO file content as a bytes vector of /// the MO file version with big endianess fn as_bytes_be(&self) -> Cow<[u8]> { let mofile = MOFile::from(self); let result = mofile.as_bytes_with(MAGIC_SWAPPED, 0); Cow::Owned(result.into_owned()) } } impl From> for POFile { fn from(entries: Vec<&POEntry>) -> Self { let mut file = POFile::new("".into()); for entry in entries { file.entries.push(entry.clone()); } file } } impl From<&Path> for POFile { fn from(path: &std::path::Path) -> Self { POFile::from(path.to_str().unwrap()) } } #[cfg(test)] mod tests { use super::*; use crate::file::mofile::mofile; use std::fs; use std::path::Path; use unicode_width::UnicodeWidthStr; #[test] fn pofile_test() { let path = "tests-data/all.po"; let file = pofile(path).unwrap(); assert_eq!(file.entries.len(), 9); } #[test] fn pofile_metadata_as_entry() { // File with metadata let path = "tests-data/all.po"; let file = pofile(path).unwrap(); let entry = file.metadata_as_entry(); assert_eq!(entry.msgid, ""); assert_eq!(entry.msgstr.unwrap().lines().count(), 11); // File without metadata let path = "tests-data/empty-metadata.po"; let file = pofile(path).unwrap(); let entry = file.metadata_as_entry(); assert_eq!(entry.msgid, ""); assert_eq!(entry.msgstr.is_none(), true); // File with fuzzy metadata let path = "tests-data/fuzzy-header.po"; let file = pofile(path).unwrap(); let entry = file.metadata_as_entry(); assert_eq!(entry.msgid, ""); assert_eq!(entry.fuzzy(), true); assert_eq!(entry.msgstr.unwrap().lines().count(), 12); } #[test] fn metadata_keys_are_natural_sorted() { let path = "tests-data/natural-unsorted-metadata.po"; let file = pofile(path).unwrap(); file.save("foobar-2-out.po"); assert_eq!( file.to_string(), "# msgid \"\" msgstr \"\" \"Project-Id-Version: PACKAGE VERSION\\n\" \"Report-Msgid-Bugs-To: \\n\" \"Language-Team: LANGUAGE \\n\" \"Content-Type: text/plain; charset=UTF-8\\n\" \"Content-Transfer-Encoding: 8bit\\n\" \"X-Poedit-SearchPath-1: Foo\\n\" \"X-Poedit-SearchPath-2: Bar\\n\" \"X-Poedit-SearchPath-10: Baz\\n\" ", ); } #[test] fn pofile_percent_translated() { let path = "tests-data/2-translated-entries.po"; let file = pofile(path).unwrap(); assert_eq!(file.percent_translated(), 40 as f32); } #[test] fn pofile_translated_entries() { let path = "tests-data/2-translated-entries.po"; let file = pofile(path).unwrap(); let translated_entries = file.translated_entries(); assert_eq!(file.entries.len(), 5); assert_eq!(translated_entries.len(), 2); assert_eq!(file.entries[0].msgid, "msgid 1"); assert_eq!(translated_entries[0].msgid, "msgid 2"); } #[test] fn pofile_untranslated_entries() { let path = "tests-data/2-translated-entries.po"; let file = pofile(path).unwrap(); let untranslated_entries = file.untranslated_entries(); assert_eq!(file.entries.len(), 5); assert_eq!(untranslated_entries.len(), 3); assert_eq!(file.entries[0].msgid, "msgid 1"); assert_eq!(untranslated_entries[0].msgid, "msgid 1"); assert_eq!(untranslated_entries[1].msgid, "msgid 3"); } #[test] fn pofile_obsolete_entries() { let path = "tests-data/obsoletes.po"; let file = pofile(path).unwrap(); let obsolete_entries = file.obsolete_entries(); assert_eq!(file.entries.len(), 3); assert_eq!(obsolete_entries.len(), 2); } #[test] fn pofile_to_string() { let po_path = "tests-data/all.po"; let file = pofile(po_path).unwrap(); let file_as_string = file.to_string(); for line in file_as_string.lines() { let width = UnicodeWidthStr::width(line); assert!(width <= file.options.wrapwidth + 2); } } fn pofile_save_test(save_fn_name: &str, fname: &str) { let tmpdir = "tests-data/tests"; let path = "tests-data/all.po"; let file = pofile(path).unwrap(); let file_as_string = file.to_string(); // Here the file name is parametrized to avoid data races // when running tests in parallel let tmp_path = Path::new(&tmpdir).join(fname); let tmp_path_str = tmp_path.to_str().unwrap(); if save_fn_name == "save" { file.save(tmp_path_str); } else { file.save_as_pofile(tmp_path_str); } assert_eq!( file_as_string, fs::read_to_string(tmp_path_str).unwrap() ); fs::remove_file(tmp_path_str).ok(); } #[test] fn pofile_save() { pofile_save_test("save", "all-1.po") } #[test] fn pofile_save_as_pofile() { pofile_save_test("save_as_pofile", "all-2.po") } #[test] fn pofile_save_as_mofile() { let tmpdir = "tests-data/tests"; let content = concat!("msgid \"foo bar\"\n", "msgstr \"foo bar\"\n",); let po_file = pofile(content).unwrap(); let tmp_path = Path::new(&tmpdir) .join("pofile_save_as_mofile-simple.mo"); let tmp_path_str = tmp_path.to_str().unwrap(); po_file.save_as_mofile(tmp_path_str); assert!(tmp_path.exists()); let mo_file = mofile(tmp_path_str).unwrap(); assert_eq!(mo_file.entries.len(), po_file.entries.len()); assert_eq!(mo_file.metadata.len(), po_file.metadata.len()); assert_eq!(mo_file.entries[0].msgid, "foo bar"); assert_eq!( mo_file.entries[0].msgstr.as_ref().unwrap(), "foo bar" ); } #[test] fn set_fuzzy() { let path = "tests-data/fuzzy-no-fuzzy.po"; let mut file = pofile(path).unwrap(); assert!(!file.entries[0].fuzzy()); assert!(file.entries[1].fuzzy()); // set fuzzy file.entries[0].flags.push("fuzzy".to_string()); // unset fuzzy let fuzzy_position = file.entries[1] .flags .iter() .position(|p| p == "fuzzy") .unwrap(); file.entries[1].flags.remove(fuzzy_position); assert!(file.entries[0].fuzzy()); assert!(!file.entries[1].fuzzy()); assert_eq!( file.entries[0].to_string(), "#, fuzzy\nmsgid \"a\"\nmsgstr \"a\"\n", ); assert_eq!( file.entries[1].to_string(), "msgid \"Line\"\nmsgstr \"Ligne\"\n", ); } #[test] fn format_fuzzy_metadata() { let path = "tests-data/fuzzy-header.po"; let file = pofile(path).unwrap(); let expected_start = concat!( "# Po file with\n# a fuzzy header\n#, fuzzy\n", "msgid \"\"\nmsgstr \"\"\n\"Project-Id-Version:", ); assert!(file.to_string().starts_with(expected_start)); } #[test] fn format_comment_ordering() { let path = "tests-data/comment-ordering.po"; let file = pofile(path).unwrap(); let expected_content = r#"# msgid "" msgstr "" "Content-Type: text/plain; charset=UTF-8\n" # First comment line #. Second comment line msgid "foo" msgstr "oof" "#; assert_eq!(file.to_string(), expected_content); } #[test] fn remove() { let mut entry_1 = POEntry::from("msgid 1"); entry_1.msgstr = Some("msgstr 1".to_string()); let mut entry_2 = POEntry::from("msgid 2"); entry_2.msgstr = Some("msgstr 2".to_string()); let mut file = POFile::from(vec![&entry_1, &entry_2]); assert_eq!(file.entries.len(), 2); // remove by entry file.remove(&entry_1); assert_eq!(file.entries.len(), 1); assert_eq!(file.entries[0].msgid, "msgid 2"); file.entries.push(entry_1); assert_eq!(file.entries.len(), 2); // remove by msgid file.remove_by_msgid("msgid 2"); assert_eq!(file.entries.len(), 1); assert_eq!(file.entries[0].msgid, "msgid 1"); // remove by msgid and msgctxt entry_2.msgctxt = Some("msgctxt 2".to_string()); entry_2.msgid = "msgid 1".to_string(); file.entries.push(entry_2); assert_eq!(file.entries.len(), 2); file.remove_by_msgid_msgctxt("msgid 1", "msgctxt 2"); assert_eq!(file.entries.len(), 1); assert_eq!(file.entries[0].msgid, "msgid 1"); assert_eq!( file.entries[0].msgstr.as_ref().unwrap(), "msgstr 1", ); } #[test] fn find() { let mut entry_1 = POEntry::new(0); entry_1.msgid = "msgid 1".to_string(); entry_1.msgstr = Some("msgstr 1".to_string()); let mut entry_2 = POEntry::new(3); entry_2.msgid = "msgid 2".to_string(); entry_2.msgstr = Some("msgstr 2".to_string()); let mut file = POFile::from(vec![&entry_1, &entry_2]); assert_eq!(file.entries.len(), 2); // find by msgid assert_eq!( file.find_by_msgid("msgid 2").unwrap().msgid, "msgid 2" ); // find by msgid and msgctxt entry_2.msgctxt = Some("msgctxt 2".to_string()); entry_2.msgid = "msgid 1".to_string(); file.entries.push(entry_2); assert_eq!(file.entries.len(), 3); assert_eq!( file.find_by_msgid_msgctxt("msgid 1", "msgctxt 2") .unwrap() .msgstr .as_ref() .unwrap(), "msgstr 2", ); // find by msgid_plural, msgctxt, msgid... let mut entry_3 = POEntry::new(6); entry_3.msgid = "msgid for msgid_plural 1".to_string(); entry_3.msgid_plural = Some("msgid_plural 1".to_string()); entry_3.msgctxt = Some("msgctxt for msgid_plural 1".to_string()); file.entries.push(entry_3); let mut entry_4 = POEntry::new(6); entry_4.msgid = "msgid for msgid_plural 1".to_string(); entry_4.msgid_plural = Some("msgid_plural 1".to_string()); entry_4.msgctxt = Some("other_msgctxt".to_string()); file.entries.push(entry_4); let entries = file.find( "msgid for msgid_plural 1", "msgid", None, false, ); assert_eq!(entries.len(), 2); let entries = file.find( "msgid for msgid_plural 1", "msgid", Some("msgctxt for msgid_plural 1"), false, ); assert_eq!(entries.len(), 1); assert_eq!( entries[0].msgctxt.as_ref().unwrap(), "msgctxt for msgid_plural 1" ); } #[test] fn parse_escapes_are_unescaped_on_format() { let path = "tests-data/escapes.po"; let file = pofile(path).unwrap(); let expected_content = "\\ \t \r \u{8} \n \\\n \u{11} \u{12} \\\\"; assert_eq!(file.entries.len(), 1); assert_eq!(file.entries[0].msgid, expected_content); assert_eq!( file.entries[0].msgstr.as_ref().unwrap(), expected_content, ); } #[test] fn parse_and_format_escapes() { let path = "tests-data/escapes.po"; let out_path = "tests-data/tests/parse_and_format_escapes.po"; let file = pofile(path).unwrap(); file.save(out_path); let escapes_content = fs::read_to_string(path).unwrap(); let out_content = fs::read_to_string(out_path).unwrap(); assert_eq!( escapes_content.replace("\r\n", "\n"), out_content ); } } rspolib-0.1.1/src/lib.rs000064400000000000000000000065621046102023000132110ustar 00000000000000//! [![crates.io](https://img.shields.io/crates/v/rspolib?logo=rust)](https://crates.io/crates/rspolib) [![PyPI](https://img.shields.io/pypi/v/rspolib?logo=pypi&logoColor=white)](https://pypi.org/project/rspolib) [![docs.rs](https://img.shields.io/docsrs/rspolib?logo=docs.rs)](https://docs.rs/rspolib) [![Bindings docs](https://img.shields.io/badge/bindings-docs-blue?logo=python&logoColor=white)](https://github.com/mondeja/rspolib/blob/master/python/REFERENCE.md) //! //! Port to Rust of the Python library [polib]. //! //! ## Install //! //! ```bash //! cargo add rspolib //! ``` //! //! ## Usage //! //! ```rust //! use rspolib::{pofile, prelude::*}; //! //! let po = pofile("./tests-data/flags.po").unwrap(); //! //! for entry in &po.entries { //! println!("{}", entry.msgid); //! } //! //! po.save("./file.po"); //! ``` //! //! See the documentation at [docs.rs/rspolib](https://docs.rs/rspolib) //! //! ## Python bindings //! //! [![Python versions](https://img.shields.io/pypi/pyversions/rspolib?logo=python&logoColor=white)](https://pypi.org/project/rspolib/#files) //! //! - [Quickstart](https://github.com/mondeja/rspolib/tree/master/python#readme) //! - [Reference](https://github.com/mondeja/rspolib/blob/master/python/REFERENCE.md) //! //! [polib]: https://github.com/izimobil/polib //! //! ## Quick examples //! //! ### Read and save a PO file //! //! ```rust //! use rspolib::{pofile, Save}; //! //! let file = pofile("tests-data/obsoletes.po").unwrap(); //! for entry in file.translated_entries() { //! println!("{}", &entry.msgid); //! } //! file.save("tests-data/docs/pofile_save.po"); //! ``` //! //! ### Read and save a MO file //! //! ```rust //! // You can include the prelude to access to all the methods //! use rspolib::{mofile, prelude::*}; //! //! let mut file = mofile("tests-data/all.mo").unwrap(); //! for entry in &file.entries { //! // All entries are translated in MO files //! println!("{}", entry.msgid); //! } //! file.save("tests-data/docs/mofile_save.mo"); //! ``` //! //! ## Features //! //! * Unicode Line Breaking formatting support. //! * Correct handling of empty and non existent PO fields values. //! * Detailed error handling parsing PO and MO files. //! * Custom byte order MO files generation. //! //! ## General view //! //! * [POFile]s, contains [POEntry]s. //! * [MOFile]s, contains [MOEntry]s. //! //! Items of the same level can be converted between them, //! for example a [POEntry] can be converted to a [MOEntry] using //! `MOEntry::from(&POEntry)` because [MOEntry]s implement the //! [From] trait for &[POEntry]. //! //! All of the conversions that make sense are implemented for //! all the structs. For example, to get the representation of a //! [POFile] just call `to_string()` or to get the binary representation //! of bytes of a [MOFile] calls `as_bytes()`. //! //! [polib]: https://github.com/izimobil/polib mod entry; pub mod errors; #[doc(hidden)] pub mod escaping; mod file; mod moparser; mod poparser; pub mod prelude; mod traits; mod twrapper; pub use crate::entry::{ mo_metadata_entry_to_string, po_metadata_entry_to_string, EntryCmpByOptions, MOEntry, MsgidEotMsgctxt, POEntry, Translated as TranslatedEntry, }; pub use crate::file::{ mofile::{mofile, MOFile}, pofile::{pofile, POFile}, AsBytes, FileOptions, Save, SaveAsMOFile, SaveAsPOFile, }; pub use crate::moparser::{MAGIC, MAGIC_SWAPPED}; pub use crate::traits::Merge; rspolib-0.1.1/src/moparser.rs000064400000000000000000000371441046102023000142730ustar 00000000000000use std::borrow::Cow; use std::fs::File; use std::io::{Cursor, SeekFrom}; use std::path::Path; use crate::entry::MOEntry; use crate::errors::IOError; use crate::file::{mofile::MOFile, FileOptions}; use crate::traits::SeekRead; /// Magic number of little endian mo files encoding /// /// Number that when found reading the four first bits read as unsigned /// 32 bits little endian of a mo file indicates that the file is in little /// endian encoding. /// /// Value as decimal: `2500072158` pub const MAGIC: u32 = 0x950412de; /// Magic number of big endian mo files encoding /// /// Number that when found reading the four first bits read as unsigned /// 32 bits little endian of a mo file indicates that the file is in big /// endian encoding. /// /// Value as decimal: `3725722773` pub const MAGIC_SWAPPED: u32 = 0xde120495; type MsgsIndex = Vec<(u32, u32)>; fn maybe_extract_plurals_from_msgid_msgstr<'a>( msgid: &'a str, msgstr: &str, ) -> (Cow<'a, str>, Option, Vec) { if !msgid.contains('\u{0}') { return (msgid.into(), None, vec![]); } let msgid_tokens = msgid.split('\u{0}').collect::>(); let (msgid, msgid_plural) = (msgid_tokens[0], msgid_tokens[1]); let msgstr_plural = msgstr .split('\u{0}') .map(|s| s.into()) .collect::>(); (msgid.into(), Some(msgid_plural.into()), msgstr_plural) } fn maybe_extract_msgctxt_from_msgid( msgid: &str, ) -> (Cow<'_, str>, Option) { let msgid_tokens = msgid.split('\x04').collect::>(); if msgid_tokens.len() == 2 { (msgid_tokens[0].into(), Some(msgid_tokens[1].to_string())) } else { (msgid.into(), None) } } /// Parser for MO files pub(crate) struct MOFileParser<'a> { /// File handler fhandle: Box, /// Function to read 4 bytes from the file freader: &'a dyn Fn([u8; 4]) -> u32, /// Parsed MO file pub file: MOFile, } impl MOFileParser<'_> { pub fn new<'a>(file_options: FileOptions) -> MOFileParser<'a> { let mut file = MOFile::new(file_options); let fhandle: Box = match Path::new( &file.options.path_or_content, ) .is_file() { true => Box::new( File::open(&file.options.path_or_content).unwrap(), ), false => Box::new(Cursor::new( file.options.byte_content.as_mut().unwrap().clone(), )), }; MOFileParser { fhandle, freader: &u32::from_le_bytes, file, } } pub fn parse(&mut self) -> Result<(), IOError> { // Parse magic number self.parse_magic_number()?; // Parse revision number self.file.version = Some(self.parse_revision_number()?); // Get number of strings let number_of_strings = self.parse_numofstrings()?; // Get messages offsets let (msgids_table_offset, msgstrs_table_offset) = self.parse_tables_offsets()?; // Parse messages indexes let (msgids_index, msgstrs_index) = self.parse_msgs_indexes( number_of_strings, msgids_table_offset, msgstrs_table_offset, )?; // Parse messages self.parse_msgs( number_of_strings, msgids_index, msgstrs_index, ); Ok(()) } fn parse_4_bytes(&mut self) -> Result { let mut buffer = [0; 4]; self.fhandle.read_exact(&mut buffer)?; Ok((self.freader)(buffer)) } fn parse_magic_number(&mut self) -> Result<(), IOError> { match self.parse_4_bytes() { Ok(magic_number) => { if magic_number == MAGIC_SWAPPED { self.freader = &u32::from_be_bytes; } else if magic_number != MAGIC { return Err(IOError::IncorrectMagicNumber { magic_number_le: magic_number, magic_number_be: u32::from_be_bytes( u32::to_le_bytes(magic_number), ), }); } self.file.magic_number = Some(magic_number); Ok(()) } Err(_e) => Err(IOError::ErrorReadingMagicNumber {}), } } fn parse_revision_number(&mut self) -> Result { match self.parse_4_bytes() { Ok(version) => { // from MO file format specs: "A program seeing an unexpected major // revision number should stop reading the MO file entirely" let available_versions: [u32; 2] = [0, 1]; if !available_versions.contains(&version) { return Err( IOError::UnsupportedMORevisionNumber { version, }, ); } Ok(version) } Err(_e) => Err(IOError::CorruptedMOData { context: "parsing revision number".to_string(), }), } } fn parse_numofstrings(&mut self) -> Result { match self.parse_4_bytes() { Ok(number_of_strings) => Ok(number_of_strings), Err(_e) => Err(IOError::CorruptedMOData { context: "parsing number of strings".to_string(), }), } } fn parse_tables_offsets( &mut self, ) -> Result<(u32, u32), IOError> { let msgids_table_offset = match self.parse_4_bytes() { Ok(offset) => offset, Err(_e) => { return Err(IOError::CorruptedMOData { context: "parsing msgids table offset" .to_string(), }) } }; let msgstrs_table_offset = match self.parse_4_bytes() { Ok(offset) => offset, Err(_e) => { return Err(IOError::CorruptedMOData { context: "parsing msgstrs table offset" .to_string(), }) } }; Ok((msgids_table_offset, msgstrs_table_offset)) } fn parse_indexes_table( &mut self, number_of_strings: u32, table_offset: u32, context: &str, ) -> Result, IOError> { self.fhandle.seek(SeekFrom::Start(table_offset as u64)).ok(); let mut indexes: Vec<(u32, u32)> = vec![]; for i in 0..number_of_strings { let msgid_length = match self.parse_4_bytes() { Ok(msgid_length) => msgid_length, Err(_e) => { return Err(IOError::CorruptedMOData { context: format!( "parsing {context} length at index {i}", ), }) } }; let msgid_offset = match self.parse_4_bytes() { Ok(msgid_offset) => msgid_offset, Err(_e) => { return Err(IOError::CorruptedMOData { context: format!( "parsing {context} offset at index {i}", ), }) } }; indexes.push((msgid_length, msgid_offset)); } Ok(indexes) } fn parse_msgs_indexes( &mut self, number_of_strings: u32, msgids_table_offset: u32, msgstrs_table_offset: u32, ) -> Result<(MsgsIndex, MsgsIndex), IOError> { let msgids_index = self.parse_indexes_table( number_of_strings, msgids_table_offset, "msgid", )?; let msgstrs_index = self.parse_indexes_table( number_of_strings, msgstrs_table_offset, "msgstr", )?; Ok((msgids_index, msgstrs_index)) } fn parse_msgs( &mut self, number_of_strings: u32, msgids_index: Vec<(u32, u32)>, msgstrs_index: Vec<(u32, u32)>, ) { for i in 0..number_of_strings { let (msgid_length, msgid_offset) = msgids_index[i as usize]; let (msgstr_length, msgstr_offset) = msgstrs_index[i as usize]; self.fhandle .seek(SeekFrom::Start(msgid_offset as u64)) .ok(); let mut msgid_buffer = vec![0; msgid_length as usize]; self.fhandle.read_exact(&mut msgid_buffer).ok(); let msgid = String::from_utf8_lossy(&msgid_buffer); self.fhandle .seek(SeekFrom::Start(msgstr_offset as u64)) .ok(); let mut msgstr_buffer = vec![0; msgstr_length as usize]; self.fhandle.read_exact(&mut msgstr_buffer).ok(); let msgstr = String::from_utf8_lossy(&msgstr_buffer); if i == 0 && msgid.is_empty() { // metadata entry for metadata_line in msgstr.split('\n') { let mut tokens = metadata_line.splitn(2, ':'); let metadata_key = tokens.next().unwrap_or("").to_string(); let metadata_value = tokens .next() .unwrap_or("") .trim() .to_string(); if !metadata_key.is_empty() { self.file .metadata .insert(metadata_key, metadata_value); } } continue; } // check if we have a plural entry let (msgid, msgid_plural, msgstr_plural) = maybe_extract_plurals_from_msgid_msgstr( &msgid, &msgstr, ); let msgctxt_tokens = maybe_extract_msgctxt_from_msgid(&msgid); let msgctxt = msgctxt_tokens.1; let entry = MOEntry::new( msgctxt_tokens.0.to_string(), Some(msgstr.into_owned()), msgid_plural, msgstr_plural, msgctxt, ); self.file.entries.push(entry); } } } #[cfg(test)] mod tests { use super::*; use rspolib_testing::{ create_binary_content, create_corrupted_binary_content, }; use std::fs; fn all_features_test(parser: &MOFileParser) { let po_path = "tests-data/all.po"; let po_content = fs::read_to_string(po_path).unwrap(); assert_eq!(parser.file.metadata.len(), 10); assert_eq!( parser.file.metadata.get("Project-Id-Version"), Some(&"django".to_string()) ); assert_eq!(parser.file.entries.len(), 7); let n_msgid_plural_entries = parser .file .entries .iter() .filter(|e| e.msgid_plural.is_some()) .count(); // msgid assert_eq!( po_content.matches("msgid \"").count() - 1 - po_content.matches("#~ msgid \"").count() * 2, parser.file.entries.len(), ); // msgstr assert_eq!( po_content.matches("msgstr \"").count() - 1 - po_content.matches("#~ msgstr \"").count(), parser .file .entries .iter() .filter(|e| e.msgstr.is_some()) .count() - n_msgid_plural_entries, ); // msgctxt assert_eq!( po_content.matches("msgctxt \"").count(), parser .file .entries .iter() .filter(|e| e.msgctxt.is_some()) .count(), ); // msgid_plural assert_eq!( // -1 because fuzzy entries are not included in mo files po_content.matches("msgid_plural \"").count() - 1, n_msgid_plural_entries, ); } #[test] fn parse_from_file() -> Result<(), IOError> { let mo_path = "tests-data/all.mo"; let mut parser = MOFileParser::new(mo_path.into()); parser.parse()?; all_features_test(&parser); Ok(()) } #[test] fn parse_from_bytes() -> Result<(), IOError> { let bytes = std::fs::read("tests-data/all.mo").ok().unwrap(); let mut parser = MOFileParser::new(bytes.into()); parser.parse()?; all_features_test(&parser); Ok(()) } #[test] fn error_on_invalid_magic_number() { let magic_number = 800; let data = vec![magic_number]; let content = create_binary_content(&data, true); let mut parser = MOFileParser::new(content.into()); let result = parser.parse(); assert_eq!( result, Err(IOError::IncorrectMagicNumber { magic_number_le: magic_number, magic_number_be: 537067520 }) ); } #[test] fn error_on_invalid_version_number() { let version = 234; let data = vec![MAGIC, version]; let content = create_binary_content(&data, true); let mut parser = MOFileParser::new(content.into()); let result = parser.parse(); assert_eq!( result, Err(IOError::UnsupportedMORevisionNumber { version }) ); } fn valid_revision_number_test(version: u32, magic_number: u32) { let data: Vec = vec![ magic_number, if magic_number == MAGIC { version } else { //v = 0b00000001_00000000_00000000_00000000; u32::from_be_bytes(u32::to_le_bytes(version)) }, ]; let content = create_binary_content(&data, magic_number == MAGIC); let mut parser = MOFileParser::new(content.into()); let result = parser.parse(); assert_eq!( result, Err(IOError::CorruptedMOData { context: "parsing number of strings".to_string() }) ) } #[test] fn parse_valid_revision_numbers() { valid_revision_number_test(0, MAGIC); valid_revision_number_test(0, MAGIC_SWAPPED); valid_revision_number_test(1, MAGIC); valid_revision_number_test(1, MAGIC_SWAPPED); } fn corrupted_binary_test( data: &Vec, additional_bytes: &Vec, expected_context: &str, ) { for le in [true, false] { let content = create_corrupted_binary_content( data, le, // Add a number to the binary to force a byte read error additional_bytes, ); let mut parser = MOFileParser::new(content.into()); let result = parser.parse(); assert_eq!( result, Err(IOError::CorruptedMOData { context: expected_context.to_string() }) ); } } #[test] fn error_corrupted_number_of_strings() { corrupted_binary_test( &vec![MAGIC, 0], &vec![3], "parsing number of strings", ); } #[test] fn error_corrupted_tables_offset() { corrupted_binary_test( &vec![MAGIC, 0, 7], &vec![4], "parsing msgids table offset", ); corrupted_binary_test( &vec![MAGIC, 0, 7, 50], &vec![4], "parsing msgstrs table offset", ); } } rspolib-0.1.1/src/poparser.rs000064400000000000000000001422071046102023000142730ustar 00000000000000use std::collections::HashMap; use std::fs::File; use std::hash::Hash; use std::io::{BufRead, BufReader, Lines, Read}; use std::iter::Iterator; use std::path::Path; use lazy_static::lazy_static; use crate::entry::POEntry; use crate::errors::{MaybeFilename, SyntaxError}; use crate::file::{pofile::POFile, FileOptions}; #[derive(Hash, Eq, PartialEq, Clone, Copy, Debug)] pub enum St { ST, // Beginning of the file (start) HE, // Header TC, // a translation comment GC, // a generated comment OC, // a file/line occurrence FL, // a flags line CT, // a message context PC, // a previous msgctxt PM, // a previous msgid PP, // a previous msgid_plural MI, // a msgid MP, // a msgid plural MS, // a msgstr MX, // a msgstr plural MC, // a msgid or msgstr continuation line } // Transitions are generated by the build.rs script include!(concat!(env!("OUT_DIR"), "/poparser-transitions.rs")); lazy_static! { static ref TRANSITIONS: Transitions = build_transitions(); static ref KEYWORDS: HashMap = { let mut m = HashMap::new(); m.insert("msgctxt".to_string(), &St::CT); m.insert("msgid".to_string(), &St::MI); m.insert("msgstr".to_string(), &St::MS); m.insert("msgid_plural".to_string(), &St::MP); m }; static ref PREV_KEYWORDS: HashMap = { let mut m = HashMap::new(); m.insert("msgid_plural".to_string(), &St::PP); m.insert("msgid".to_string(), &St::PM); m.insert("msgctxt".to_string(), &St::PC); m }; } /// Function to transition from a state to another in the parser type TransitionFn = dyn Fn(&mut POFileParser) -> Result<(), SyntaxError>; type Symbol = St; type CurrentSt = St; type Action = St; type NextSt = St; /// Transitions hashmap, from (symbol, current_state) to (action, next_state) pub type Transitions = HashMap<(Symbol, CurrentSt), (Action, NextSt)>; struct LinesHandler<'a> { lines: Lines>, } impl LinesHandler<'_> { fn new(handler: &mut dyn Read) -> LinesHandler { LinesHandler { lines: BufReader::new(handler).lines(), } } } impl Iterator for LinesHandler<'_> { type Item = String; fn next(&mut self) -> Option { match self.lines.next() { Some(Ok(line)) => Some(line), Some(Err(_)) => None, None => None, } } } /// PO file parser pub(crate) struct POFileParser { /// Whether the content is a path to a file or the file content content_is_path: bool, /// Parsed PO file pub file: POFile, /// Current state current_state: St, /// Current token current_token: String, /// Current line number current_line: usize, /// Current entry being constructed current_entry: POEntry, /// Current msgstr index msgstr_index: usize, /// Whether the current entry is obsolete entry_obsolete: bool, } impl POFileParser { pub fn new(file_options: FileOptions) -> POFileParser { POFileParser { content_is_path: Path::new(&file_options.path_or_content) .is_file(), file: POFile::new(file_options), current_state: St::ST, current_token: String::with_capacity(32), current_line: 0, current_entry: POEntry::new(0), msgstr_index: 0, entry_obsolete: false, } } fn add_current_entry(&mut self) -> Result<(), SyntaxError> { let unescaped_entry = self.current_entry.unescaped(); if unescaped_entry.is_err() { return Err(SyntaxError::BasicCustom { maybe_filename: MaybeFilename::new( &self.file.options.path_or_content, self.content_is_path, ), message: unescaped_entry.err().unwrap().to_string(), }); } self.file.entries.push(unescaped_entry.unwrap()); self.current_entry = POEntry::new(self.current_line); self.msgstr_index = 0; Ok(()) } fn maybe_add_current_entry(&mut self) -> Result<(), SyntaxError> { if [St::MC, St::MS, St::MX].contains(&self.current_state) { self.add_current_entry()?; } Ok(()) } fn process( &mut self, symbol: &Symbol, ) -> Result<(), SyntaxError> { let next_transition = (*symbol, self.current_state); let (action, next_state) = *TRANSITIONS.get(&next_transition).unwrap(); (transition_fn_factory(action)?)(self)?; if action != St::MC { // if not in a message continuation line, change the state self.current_state = next_state; } Ok(()) } pub fn parse(&mut self) -> Result<(), SyntaxError> { if self.content_is_path { self.parse_file()?; } else { self.parse_content()?; } Ok(()) } fn parse_file(&mut self) -> Result<(), SyntaxError> { let mut buf = BufReader::new( File::open(&self.file.options.path_or_content).unwrap(), ); let mut handler = LinesHandler::new(&mut buf); self.parse_with_handler(&mut handler)?; Ok(()) } fn parse_content(&mut self) -> Result<(), SyntaxError> { let content = self.file.options.path_or_content.clone(); let mut buf = BufReader::new(content.as_bytes()); let mut handler = LinesHandler::new(&mut buf); self.parse_with_handler(&mut handler)?; Ok(()) } fn parse_with_handler( &mut self, handler: &mut LinesHandler, ) -> Result<(), SyntaxError> { let first_line = handler.next().unwrap_or("".to_string()); self.parse_line(maybe_lstrip_utf8_bom(&first_line))?; for line in handler.by_ref() { self.parse_line(&line)?; } if self.current_entry.msgid.is_empty() { // Adding header entry if let Some(msgstr) = &self.current_entry.msgstr { if !msgstr.is_empty() { self.add_current_entry()?; } } } else { self.add_current_entry()?; } let metadata_entry = self.file.find_by_msgid(""); if let Some(metadata_entry) = metadata_entry { // Remove header from entries and store it in metadata hashmap self.file.metadata_is_fuzzy = !metadata_entry.flags.is_empty(); self.file.remove(&metadata_entry); for metadata_line in metadata_entry.msgstr.unwrap().split('\n') { let (key, value) = match metadata_line.split_once(": ") { Some((key, value)) => (key, value), None => continue, }; if !self.file.metadata.contains_key(key) { self.file.metadata.insert( key.to_string(), value.trim().to_string(), ); } else { let mut new_value = self.file.metadata.remove(key).unwrap(); new_value.push_str(value.trim()); self.file .metadata .insert(key.to_string(), new_value); } } } Ok(()) } fn tokens_from_line(&self, line: &str) -> Vec { let mut tokens: Vec = Vec::with_capacity(3); for token in line.split_ascii_whitespace() { tokens.push(token.to_string()); if tokens.len() == 3 { break; } } tokens } fn parse_line(&mut self, line: &str) -> Result<(), SyntaxError> { self.current_line += 1; let mut line = line.trim(); if line.is_empty() { return Ok(()); } let mut tokens = self.tokens_from_line(line); let mut nb_tokens = tokens.len(); if nb_tokens == 0 || tokens[0] == "#~|" { return Ok(()); } else if nb_tokens > 1 && tokens[0] == "#~" { line = line[3..].trim(); tokens = tokens[1..].to_vec(); nb_tokens -= 1; self.entry_obsolete = true } else { self.entry_obsolete = false; } if nb_tokens > 1 && KEYWORDS.contains_key(&tokens[0]) { line = line[tokens[0].len()..].trim_start(); maybe_raise_unescaped_double_quote_found_error( line, self.current_line, self.content_is_path, &self.file.options.path_or_content, // +2 taking into account ' "' (space and first // double quote) tokens[0].chars().count() + 2, )?; self.current_token = line.to_string(); let symbol = *KEYWORDS.get(&tokens[0]).unwrap(); self.process(symbol)?; return Ok(()); } self.current_token = line.to_string(); if tokens[0] == "#:" { if nb_tokens <= 1 { return Ok(()); } // occurrences self.process(&St::OC)?; } else if line.starts_with('"') { // continuation line maybe_raise_unescaped_double_quote_found_error( line, self.current_line, self.content_is_path, &self.file.options.path_or_content, 1, )?; self.process(&St::MC)?; } else if self.current_token.starts_with("msgstr[") { // msgstr plural let index = self .current_token .splitn(2, '[') .last() .unwrap() .split(']') .next() .unwrap(); match index.parse::() { Ok(index) => { self.msgstr_index = index; } Err(_) => { return Err(SyntaxError::Custom { maybe_filename: MaybeFilename::new( &self.file.options.path_or_content, self.content_is_path, ), message: format!( concat!( "Invalid msgstr plural index.", " Expected digit, found '{}'." ), index, ), line: self.current_line, index: 7, }); } }; self.process(&St::MX)?; } else if tokens[0] == "#," { if nb_tokens < 2 { return Ok(()); } // flags line self.process(&St::FL)?; } else if tokens[0] == "#" || tokens[0].starts_with("##") { //if line == "#" { // line.push(' '); //} // translator comment self.process(&St::TC)?; } else if tokens[0] == "#." { if nb_tokens < 2 { return Ok(()); } // generated comment self.process(&St::GC)?; } else if tokens[0] == "#|" { if nb_tokens < 2 { return Err(SyntaxError::Custom { maybe_filename: MaybeFilename::new( &self.file.options.path_or_content, self.content_is_path, ), line: self.current_line, index: 2, message: "empty previous message found" .to_string(), }); } // Remove the marker and any whitespace following it if tokens[1].starts_with('"') { // Continuation of previous metadata self.process(&St::MC)?; return Ok(()); } if nb_tokens == 2 { return Err(SyntaxError::Custom { message: "invalid continuation line".to_string(), maybe_filename: MaybeFilename::new( &self.file.options.path_or_content, self.content_is_path, ), line: self.current_line, index: 0, }); } // "previous translation" comment line if !PREV_KEYWORDS.contains_key(&tokens[1]) { // Unknown keyword in previous translation comment return Err(SyntaxError::Custom { message: format!("unknown keyword {}", tokens[1]), maybe_filename: MaybeFilename::new( &self.file.options.path_or_content, self.content_is_path, ), line: self.current_line, index: 0, }); } // Remove the keyword and any whitespace // between it and the starting quote self.current_token = line [tokens[1].len() + tokens[0].len() + 1..] .trim_start() .to_string(); self.process(PREV_KEYWORDS.get(&tokens[1]).unwrap())?; } else { return Err(SyntaxError::Generic { maybe_filename: MaybeFilename::new( &self.file.options.path_or_content, self.content_is_path, ), line: self.current_line, index: 0, }); } Ok(()) } } fn handle_he(parser: &mut POFileParser) -> Result<(), SyntaxError> { let h = parser.file.header.clone(); let mut newheader = h.unwrap_or_default().to_string(); if !newheader.is_empty() { newheader.push('\n'); } if parser.current_token.len() > 2 { newheader.push_str(&parser.current_token[2..]); } parser.file.header = Some(newheader); Ok(()) } fn handle_tc(parser: &mut POFileParser) -> Result<(), SyntaxError> { parser.maybe_add_current_entry()?; let optional_tcomment = parser.current_entry.tcomment.as_mut(); let mut tcomment = match optional_tcomment { Some(tcomment) => { let t = tcomment; t.push('\n'); t } None => "", } .to_string(); let mut toappend = parser.current_token.trim_start_matches('#').to_string(); if toappend.starts_with(' ') { toappend = toappend[1..].to_string(); } tcomment.push_str(&toappend); parser.current_entry.tcomment = Some(tcomment.as_str().to_string()); Ok(()) } fn handle_gc(parser: &mut POFileParser) -> Result<(), SyntaxError> { parser.maybe_add_current_entry()?; let optional_comment = parser.current_entry.comment.as_mut(); let mut comment = match optional_comment { Some(comment) => { let t = comment; t.push('\n'); t } None => "", } .to_string(); if parser.current_token.len() > 3 { comment.push_str(&parser.current_token[3..]); } parser.current_entry.comment = Some(comment); Ok(()) } fn handle_oc(parser: &mut POFileParser) -> Result<(), SyntaxError> { parser.maybe_add_current_entry()?; for occ in parser.current_token[3..].split_whitespace() { if !occ.is_empty() { let (mut fil, mut line) = occ.split_once(':').unwrap_or((occ, "")); let mut line_isdigit = true; for c in line.chars() { if !c.is_ascii_digit() { line_isdigit = false; break; } } if !line_isdigit { fil = occ; line = ""; } parser .current_entry .occurrences .push((fil.to_string(), line.to_string())) } } Ok(()) } fn handle_fl(parser: &mut POFileParser) -> Result<(), SyntaxError> { parser.maybe_add_current_entry()?; if parser.current_token.len() > 3 { let current_token_split = parser.current_token[3..].split(','); for substr in current_token_split { parser .current_entry .flags .push(substr.trim().to_string()); } } Ok(()) } fn handle_pp(parser: &mut POFileParser) -> Result<(), SyntaxError> { parser.maybe_add_current_entry()?; parser.current_entry.previous_msgid_plural = Some( parser.current_token[1..parser.current_token.len() - 1] .to_string(), ); Ok(()) } fn handle_pm(parser: &mut POFileParser) -> Result<(), SyntaxError> { parser.maybe_add_current_entry()?; parser.current_entry.previous_msgid = Some( parser.current_token[1..parser.current_token.len() - 1] .to_string(), ); Ok(()) } fn handle_pc(parser: &mut POFileParser) -> Result<(), SyntaxError> { parser.maybe_add_current_entry()?; parser.current_entry.previous_msgctxt = Some( parser.current_token[1..parser.current_token.len() - 1] .to_string(), ); Ok(()) } fn handle_ct(parser: &mut POFileParser) -> Result<(), SyntaxError> { parser.maybe_add_current_entry()?; parser.current_entry.msgctxt = Some( parser.current_token[1..parser.current_token.len() - 1] .to_string(), ); Ok(()) } fn handle_mi(parser: &mut POFileParser) -> Result<(), SyntaxError> { parser.maybe_add_current_entry()?; parser.current_entry.obsolete = parser.entry_obsolete; parser.current_entry.msgid = parser.current_token [1..parser.current_token.len() - 1] .to_string(); Ok(()) } fn handle_mp(parser: &mut POFileParser) -> Result<(), SyntaxError> { parser.current_entry.msgid_plural = Some( parser.current_token[1..parser.current_token.len() - 1] .to_string(), ); Ok(()) } fn handle_ms(parser: &mut POFileParser) -> Result<(), SyntaxError> { parser.current_entry.msgstr = Some( parser.current_token[1..parser.current_token.len() - 1] .to_string(), ); Ok(()) } fn handle_mx(parser: &mut POFileParser) -> Result<(), SyntaxError> { let value = &parser.current_token[parser.current_token.find('"').unwrap() + 1 ..parser.current_token.len() - 1]; let msgstr_plural_length = parser.current_entry.msgstr_plural.len(); if parser.msgstr_index + 1 > msgstr_plural_length { for _ in 0..parser.msgstr_index + 1 - msgstr_plural_length { parser.current_entry.msgstr_plural.push("".to_string()); } } parser.current_entry.msgstr_plural[parser.msgstr_index] = value.to_string(); Ok(()) } fn handle_mc(parser: &mut POFileParser) -> Result<(), SyntaxError> { let token = &parser.current_token[1..&parser.current_token.len() - 1]; if parser.current_state == St::MI { parser.current_entry.msgid.push_str(token); } else if parser.current_state == St::MS { let msgstr = parser.current_entry.msgstr.as_mut().unwrap(); msgstr.push_str(token); parser.current_entry.msgstr = Some(msgstr.to_string()); } else if parser.current_state == St::CT { let msgctxt = parser.current_entry.msgctxt.as_mut().unwrap(); msgctxt.push_str(token); parser.current_entry.msgctxt = Some(msgctxt.to_string()); } else if parser.current_state == St::MP { let msgid_plural = parser.current_entry.msgid_plural.as_mut().unwrap(); msgid_plural.push_str(token); parser.current_entry.msgid_plural = Some(msgid_plural.to_string()); } else if parser.current_state == St::MX { parser.current_entry.msgstr_plural[parser.msgstr_index] .push_str(token); } else if parser.current_state == St::PP { let previous_msgid_plural = parser .current_entry .previous_msgid_plural .as_mut() .unwrap(); previous_msgid_plural.push_str(token); parser.current_entry.previous_msgid_plural = Some(previous_msgid_plural.to_string()); } else if parser.current_state == St::PM { let previous_msgid = parser.current_entry.previous_msgid.as_mut().unwrap(); previous_msgid.push_str(token); parser.current_entry.previous_msgid = Some(previous_msgid.to_string()); } else if parser.current_state == St::PC { let previous_msgctxt = parser.current_entry.previous_msgctxt.as_mut().unwrap(); previous_msgctxt.push_str(token); parser.current_entry.previous_msgctxt = Some(previous_msgctxt.to_string()); } else { return Err(SyntaxError::Custom { maybe_filename: MaybeFilename::new( &parser.file.options.path_or_content, parser.content_is_path, ), message: format!( "unexpected state {:?}", parser.current_state ), line: parser.current_line, index: 0, }); } Ok(()) } fn transition_fn_factory( action: Action, ) -> Result<&'static TransitionFn, SyntaxError> { match action { St::HE => Ok(&handle_he), St::TC => Ok(&handle_tc), St::GC => Ok(&handle_gc), St::OC => Ok(&handle_oc), St::FL => Ok(&handle_fl), St::PP => Ok(&handle_pp), St::PM => Ok(&handle_pm), St::PC => Ok(&handle_pc), St::CT => Ok(&handle_ct), St::MI => Ok(&handle_mi), St::MP => Ok(&handle_mp), St::MS => Ok(&handle_ms), St::MX => Ok(&handle_mx), St::MC => Ok(&handle_mc), _ => Err(SyntaxError::UnknownState { state: format!("{action:?}"), }), } } #[inline(always)] fn maybe_lstrip_utf8_bom(line: &str) -> &str { line.trim_start_matches('\u{feff}') } fn find_unescaped_double_quote_index(line: &str) -> Option { let mut escaped = false; for (i, c) in line.chars().enumerate() { if c == '"' && !escaped { return Some(i); } else if c == '\\' { escaped = !escaped; } else { escaped = false; } } None } fn maybe_raise_unescaped_double_quote_found_error( text: &str, linenum: usize, path_or_content_is_path: bool, path_or_content: &str, index_offset: usize, ) -> Result<(), SyntaxError> { // Check if last character is not a double quote // to prevent slicing out of bounds let text_chars = text.chars(); if text_chars.last().unwrap_or('\0') != '"' { return Err(SyntaxError::Custom { maybe_filename: MaybeFilename::new( path_or_content, path_or_content_is_path, ), line: linenum, index: text.chars().count() - 1, message: format!("unterminated string '{text}'"), }); } let text_str = text.to_string(); let unescaped_double_quote_i = find_unescaped_double_quote_index( &text_str[1..text_str.len() - 1], ); if let Some(double_quote_i) = unescaped_double_quote_i { return Err(SyntaxError::UnescapedDoubleQuoteFound { maybe_filename: MaybeFilename::new( path_or_content, path_or_content_is_path, ), line: linenum, index: double_quote_i + 1 + index_offset, }); } Ok(()) } #[cfg(test)] mod tests { use super::*; use std::fs; #[test] fn constructor() { let path = "tests-data/empty.po"; let content: String = fs::read_to_string(path).unwrap(); // init from file path let parser = POFileParser::new(path.into()); assert_eq!(parser.file.options.path_or_content, path); assert_eq!(parser.content_is_path, true); assert_eq!(parser.file.options.wrapwidth, 78); assert_eq!(parser.current_line, 0); assert_eq!(parser.current_entry.msgid, ""); assert_eq!(parser.current_entry.linenum, 0); assert_eq!(parser.msgstr_index, 0); // init from file path and wrapwidth let parser = POFileParser::new((path, 30).into()); assert_eq!(parser.file.options.path_or_content, path); assert_eq!(parser.content_is_path, true); assert_eq!(parser.file.options.wrapwidth, 30); // init from file content let parser = POFileParser::new(content.as_str().into()); assert_eq!(parser.file.options.path_or_content, content); assert_eq!(parser.content_is_path, false); assert_eq!(parser.file.options.wrapwidth, 78); } #[test] fn parse_empty_file() -> Result<(), SyntaxError> { let path = "tests-data/empty.po"; let mut parser = POFileParser::new(path.into()); parser.parse()?; assert_eq!(parser.file.entries.len(), 0); assert_eq!(parser.file.metadata.len(), 0); Ok(()) } #[test] fn parse_empty_content() -> Result<(), SyntaxError> { let mut parser = POFileParser::new("".into()); parser.parse()?; assert_eq!(parser.file.entries.len(), 0); assert_eq!(parser.file.metadata.len(), 0); Ok(()) } #[test] fn parse_utf8_bom() -> Result<(), SyntaxError> { let path = "tests-data/utf8-bom.po"; let mut parser = POFileParser::new(path.into()); parser.parse()?; assert_eq!( parser.file.header, Some("This file contains an UTF8 BOM".to_string()), ); assert_eq!(parser.file.metadata.len(), 0); assert_eq!(parser.file.entries.len(), 0); Ok(()) } #[test] fn parse_header() -> Result<(), SyntaxError> { let path = "tests-data/header-no-trailing-newline.po"; let mut parser = POFileParser::new(path.into()); parser.parse()?; assert_eq!( parser.file.header, Some( concat!( "This file is distributed under the same license", "\n\n", "Translators:", "\n", "Foo bar, Year", "\n\n", "Baz, YEAR", ) .to_string() ), ); let path = "tests-data/header-trailing-newlines.po"; let mut parser = POFileParser::new(path.into()); parser.parse()?; assert_eq!( parser.file.header, Some( concat!( "This file is distributed under the same license\n\n", "Translators:\n", "Foo bar, Year\n\n", "Baz, YEAR\n\n\n", ) .to_string() ), ); // header must not be saved as entry assert_eq!(parser.file.entries.len(), 0); Ok(()) } #[test] fn parse_metadata() -> Result<(), SyntaxError> { let path = "tests-data/metadata.po"; let mut parser = POFileParser::new(path.into()); parser.parse()?; assert_eq!(parser.file.metadata.len(), 11); // metadata must not be saved as entry assert_eq!(parser.file.entries.len(), 0); // check metadata let metadata = HashMap::from([ ("Plural-Forms", "nplurals=2; plural=(n != 1);"), ("POT-Creation-Date", "2020-05-19 20:23+0200"), ("Content-Transfer-Encoding", "8bit"), ("MIME-Version", "1.0"), ("Report-Msgid-Bugs-To", "mondeja"), ("PO-Revision-Date", "2020-09-28 03:17+0000"), ("Project-Id-Version", "django"), ("Last-Translator", "Foo Bar "), ("Content-Type", "text/plain; charset=UTF-8"), ("Language", "es"), ( "Language-Team", concat!( "Spanish", " (http://www.transifex.com/", "django/django/language/es/)", ), ), ]); for (key, value) in metadata.iter() { assert_eq!( parser.file.metadata.get(&key as &str).unwrap(), value ); } Ok(()) } #[test] fn parse_msgids_msgstrs() -> Result<(), SyntaxError> { let path = "tests-data/msgids-msgstrs.po"; let mut parser = POFileParser::new(path.into()); parser.parse()?; assert_eq!(parser.file.entries.len(), 2); let first_entry = &parser.file.entries[0]; let second_entry = &parser.file.entries[1]; assert_eq!(first_entry.msgid, "msgid 1"); assert_eq!(first_entry.msgstr.as_ref().unwrap(), "msgstr 1"); assert_eq!(first_entry.obsolete, false); assert_eq!(second_entry.msgid, "msgid 2"); assert_eq!(second_entry.msgstr.as_ref().unwrap(), "msgstr 2"); assert_eq!(second_entry.obsolete, false); Ok(()) } #[test] fn parse_long_message() -> Result<(), SyntaxError> { let path = "tests-data/long-message.po"; let mut parser = POFileParser::new(path.into()); parser.parse()?; assert_eq!(parser.file.entries.len(), 1); let entry = &parser.file.entries[0]; assert_eq!( entry.msgid, concat!( "Enter a valid “slug” consisting of letters, numbers,", " underscores or hyphens.", ), ); assert_eq!( entry.msgstr.as_ref().unwrap(), concat!( "Introduzca un 'slug' válido, consistente en letras,", " números, guiones bajos o medios.", ), ); assert_eq!( entry.comment.as_ref().unwrap(), "This is a generated/extracted comment", ); Ok(()) } #[test] fn parse_long_msgids_msgstrs() -> Result<(), SyntaxError> { let path = "tests-data/msgid-msgstr-long.po"; let mut parser = POFileParser::new(path.into()); parser.parse()?; let po_content = fs::read_to_string(path).unwrap(); assert_eq!(parser.file.entries.len(), 2); assert_eq!(parser.file.metadata.len(), 0); let first_entry = &parser.file.entries[0]; let second_entry = &parser.file.entries[1]; // first entry, defined in one line at po file assert_eq!( first_entry.msgid, concat!( "msgid 1 msgid 1 msgid 1 msgid 1 msgid 1", " msgid 1 msgid 1 msgid 1 msgid 1 msgid 1", " msgid 1 msgid 1", ) ); assert_eq!( first_entry.msgid.len(), po_content.lines().nth(0).unwrap().len() - "msgid ".len() - 2, ); let msgstr = first_entry.msgstr.as_ref().unwrap(); assert_eq!( msgstr, concat!( "msgstr 1 msgstr 1 msgstr 1 msgstr 1 msgstr 1", " msgstr 1 msgstr 1 msgstr 1 msgstr 1 msgstr 1", " msgstr 1", ) ); assert_eq!( msgstr.len(), po_content.lines().nth(1).unwrap().len() - "msgstr ".len() - 2, ); // second entry, wrapped at po file let expected_msgid_msgstr = concat!( "\n

To install bookmarklets, drag", " the link to your bookmarks\ntoolbar, or right-click", " the link and add it to your bookmarks. Now you can\n", "select the bookmarklet from any page in the site.", " Note that some of these\nbookmarklets require you to", " be viewing the site from a computer designated\n", "as \"internal\" (talk to your system administrator", " if you aren't sure if\nyour computer is \"internal\").", "

\n", ); assert_eq!(second_entry.msgid, expected_msgid_msgstr); assert_eq!( second_entry.msgstr.as_ref().unwrap(), expected_msgid_msgstr ); Ok(()) } #[test] fn parse_flags() -> Result<(), SyntaxError> { let path = "tests-data/flags.po"; let mut parser = POFileParser::new(path.into()); parser.parse()?; assert_eq!(parser.file.entries.len(), 6); let entry_1 = &parser.file.entries[0]; let entry_2 = &parser.file.entries[1]; let entry_3 = &parser.file.entries[2]; let entry_4 = &parser.file.entries[3]; let entry_5 = &parser.file.entries[4]; let entry_6 = &parser.file.entries[5]; assert_eq!(entry_1.msgid, "msgid 1"); assert_eq!(entry_1.msgstr.as_ref().unwrap(), "msgstr 1"); assert_eq!(entry_1.obsolete, false); assert_eq!(entry_1.flags.len(), 2); assert_eq!(entry_1.flags, vec!["python-format", "fuzzy"]); assert_eq!(entry_1.fuzzy(), true); assert_eq!(entry_2.msgid, "msgid 2"); assert_eq!(entry_2.msgstr.as_ref().unwrap(), "msgstr 2"); assert_eq!(entry_2.obsolete, false); assert_eq!(entry_2.flags.len(), 1); assert_eq!(entry_2.flags[0], "fuzzy"); assert_eq!(entry_2.fuzzy(), true); assert_eq!(entry_3.msgid, "msgid 3"); assert_eq!(entry_3.msgstr.as_ref().unwrap(), "msgstr 3"); assert_eq!(entry_3.obsolete, false); assert_eq!(entry_3.flags.len(), 1); assert_eq!(entry_3.flags[0], "python-format"); assert_eq!(entry_3.fuzzy(), false); assert_eq!(entry_4.msgid, "msgid 4"); assert_eq!(entry_4.msgstr.as_ref().unwrap(), "msgstr 4"); assert_eq!(entry_4.obsolete, false); assert_eq!(entry_4.flags.len(), 7); assert_eq!( entry_4.flags, vec!["1", "2", "3", "4", "5", "6", "7"] ); assert_eq!(entry_4.fuzzy(), false); assert_eq!(entry_5.msgid, "msgid 5"); assert_eq!(entry_5.msgstr.as_ref().unwrap(), "msgstr 5"); assert_eq!(entry_5.obsolete, false); assert_eq!(entry_5.flags.len(), 7); assert_eq!( entry_5.flags, vec!["a", "b", "c", "d", "e", "f", "g"] ); assert_eq!(entry_5.fuzzy(), false); assert_eq!(entry_6.msgid, "msgid 6"); assert_eq!(entry_6.msgstr.as_ref().unwrap(), "msgstr 6"); assert_eq!(entry_6.obsolete, false); assert_eq!(entry_6.flags.len(), 0); assert_eq!(entry_6.fuzzy(), false); Ok(()) } #[test] fn parse_msgid_plural() -> Result<(), SyntaxError> { let path = "tests-data/msgid-plural.po"; let mut parser = POFileParser::new(path.into()); parser.parse()?; assert_eq!(parser.file.entries.len(), 2); assert_eq!( parser.file.entries[0].msgid_plural.is_some(), true ); assert_eq!( parser.file.entries[1].msgid_plural.is_some(), true ); let entry_1 = &parser.file.entries[0]; let entry_2 = &parser.file.entries[1]; // check msgid_plural assert_eq!( entry_1.msgid_plural.as_ref().unwrap(), concat!( "A Ensure this value has at least %(limit_value)d", " characters (it has %(show_value)d).", ) ); assert_eq!( entry_2.msgid_plural.as_ref().unwrap(), concat!( "B Ensure this value has at least %(limit_value)d", " characters (it has %(show_value)d).", ) ); // check msgstr_plural assert_eq!(entry_1.msgstr_plural.len(), 2); assert_eq!( entry_1.msgstr_plural[0], concat!( "A Asegúrese de que este valor tenga al menos", " %(limit_value)d caracter (tiene %(show_value)d).", ) ); assert_eq!( entry_1.msgstr_plural[1], concat!( "A Asegúrese de que este valor tenga al menos", " %(limit_value)d carácter(es) (tiene%(show_value)d).", ) ); Ok(()) } #[test] fn parse_msgctxt() -> Result<(), SyntaxError> { let path = "tests-data/msgctxt.po"; let mut parser = POFileParser::new(path.into()); parser.parse()?; assert_eq!(parser.file.entries.len(), 3); let entry_1 = &parser.file.entries[0]; let entry_2 = &parser.file.entries[1]; let entry_3 = &parser.file.entries[2]; assert_eq!(entry_1.msgid, "Jan."); assert_eq!(entry_1.msgstr.as_ref().unwrap(), "Ene."); assert_eq!( entry_1.msgctxt.as_ref().unwrap(), "abbrev. month" ); assert_eq!(entry_1.fuzzy(), false); assert_eq!(entry_2.msgid, "J."); assert_eq!(entry_2.msgstr.as_ref().unwrap(), "E."); assert_eq!( entry_2.msgctxt.as_ref().unwrap(), "abbrev. month" ); assert_eq!(entry_2.fuzzy(), true); assert_eq!(entry_3.msgid, "To date"); assert_eq!( entry_3.msgstr.as_ref().unwrap(), "Hasta la fecha" ); assert_eq!(entry_3.msgctxt.as_ref().unwrap(), "to date"); assert_eq!(entry_3.fuzzy(), false); Ok(()) } #[test] fn parse_previous_msgid_msgctx() -> Result<(), SyntaxError> { let path = "tests-data/previous-msgid-msgctxt.po"; let mut parser = POFileParser::new(path.into()); parser.parse()?; assert_eq!(parser.file.entries.len(), 1); assert_eq!(parser.file.entries[0].msgid, "Some msgid"); assert_eq!( parser.file.entries[0].previous_msgid.as_ref().unwrap(), "previous untranslated entry" ); assert_eq!( parser.file.entries[0].previous_msgctxt.as_ref().unwrap(), "@previous_context" ); Ok(()) } #[test] fn parse_empty_occurrences_line() -> Result<(), SyntaxError> { let path = "tests-data/empty-occurrences-line.po"; let mut parser = POFileParser::new(path.into()); parser.parse()?; assert_eq!(parser.file.entries.len(), 1); let expected_occurrences = Vec::from([ ("db/models/manipulators.py", "310"), ("contrib/admin/views/main.py", "342"), ("contrib/admin/views/main.py", "344"), ("contrib/admin/views/main.py", "346"), ("core/validators.py", "275"), ]); assert_eq!(parser.file.entries[0].occurrences.len(), 5); for (i, (occ_fline, occ_line)) in expected_occurrences.iter().enumerate() { assert_eq!( parser.file.entries[0].occurrences[i], (occ_fline.to_string(), occ_line.to_string()) ); } Ok(()) } #[test] fn parse_occurrence_no_linenum() -> Result<(), SyntaxError> { // Parse a occurrence line with no line number let path = "tests-data/occurrence-no-linenum.po"; let mut parser = POFileParser::new(path.into()); parser.parse()?; assert_eq!(parser.file.entries.len(), 1); assert_eq!(parser.file.entries[0].msgid, "Hello"); assert_eq!( parser.file.entries[0].msgstr.as_ref().unwrap(), "Bonjour" ); assert_eq!(parser.file.entries[0].occurrences.len(), 2); assert_eq!( parser.file.entries[0].occurrences[0], ("path/to/file/noocc.rs".to_string(), "".to_string()) ); assert_eq!( parser.file.entries[0].occurrences[1], ("path/to/file/occ.rs".to_string(), "45".to_string()) ); Ok(()) } #[test] fn parse_weird_occurrences() -> Result<(), SyntaxError> { let path = "tests-data/weird-occurrences.po"; let mut parser = POFileParser::new(path.into()); parser.parse()?; assert_eq!(parser.file.entries.len(), 3); let entry_1 = &parser.file.entries[0]; assert_eq!(entry_1.msgid, "Windows path"); assert_eq!( entry_1.occurrences, vec![("C:\\foo\\bar.py:12".to_string(), "".to_string())] ); let entry_2 = &parser.file.entries[1]; assert_eq!(entry_2.msgid, "Override the default prgname"); assert_eq!( entry_2.occurrences, vec![("main.c".to_string(), "117".to_string())] ); let entry_3 = &parser.file.entries[2]; assert_eq!(entry_3.msgid, "choose new graphic"); assert_eq!(entry_3.occurrences, vec![ ("Balloon-Fills,BitmapFillStyle>>addFillStyleMenuItems:hand:from:".to_string(), "".to_string()) ]); Ok(()) } #[test] fn parse_complete() -> Result<(), SyntaxError> { let path = "tests-data/django-complete.po"; let content = fs::read_to_string(path).unwrap(); let mut parser = POFileParser::new(content.as_str().into()); parser.parse()?; assert_eq!(parser.file.entries.len(), 341); assert_eq!(parser.file.header.unwrap().lines().count(), 30); let n_msgid_plurals = parser .file .entries .iter() .filter(|e| e.msgid_plural.is_some()) .count(); // check msgids assert_eq!( // -1 ignoring the header content.matches("msgid \"").count() - 1, parser.file.entries.len(), ); // check msgstrs assert_eq!( // -1 ignoring the header content.matches("msgstr").count() - 1 - n_msgid_plurals, parser.file.entries.len(), ); // check msgctxts assert_eq!( content.matches("msgctxt \"").count(), parser .file .entries .iter() .filter(|e| e.msgctxt.is_some()) .count(), ); // check plurals assert_eq!( content.matches("msgid_plural \"").count(), n_msgid_plurals ); assert_eq!( content.matches("msgstr[0]").count(), n_msgid_plurals ); assert_eq!( content.matches("msgstr[1]").count(), n_msgid_plurals ); // number of flags assert_eq!( content.matches("#, ").count(), parser .file .entries .iter() .filter(|e| e.flags.len() > 0) .count(), ); // number of 'python-format' flags assert_eq!( content.matches("python-format").count(), parser .file .entries .iter() .filter(|e| e.flags.contains(&"python-format".into())) .count(), ); Ok(()) } #[test] fn parse_fuzzy_header() -> Result<(), SyntaxError> { let path = "tests-data/fuzzy-header.po"; let mut parser = POFileParser::new(path.into()); parser.parse()?; let metadata_as_entry = parser.file.metadata_as_entry(); assert_eq!(parser.file.entries.len(), 0); assert_eq!(parser.file.header.unwrap().lines().count(), 2); assert_eq!(metadata_as_entry.fuzzy(), true); Ok(()) } #[test] fn parse_indented() -> Result<(), SyntaxError> { let path = "tests-data/indented.po"; let mut parser = POFileParser::new(path.into()); parser.parse()?; assert_eq!(parser.file.entries.len(), 2); assert_eq!( parser.file.entries[0].tcomment.as_ref().unwrap(), concat!( "Added for previous msgid/msgid_plural/msgctxt testing", "\nTokens are separated by some tabs and a single space.", ), ); Ok(()) } #[test] fn parse_previous_continuation_line() -> Result<(), SyntaxError> { let path = "tests-data/previous-msgid-continuation.po"; let mut parser = POFileParser::new(path.into()); parser.parse()?; assert_eq!(parser.file.entries.len(), 2); Ok(()) } #[test] fn parse_repeated_metadata() -> Result<(), SyntaxError> { let path = "tests-data/repeated-metadata-keys.po"; let mut parser = POFileParser::new(path.into()); parser.parse()?; assert!(parser .file .metadata .contains_key("Content-Transfer-Encoding")); assert_eq!( parser .file .metadata .get("Content-Transfer-Encoding") .unwrap(), r"8bit4bit", ); assert!(parser.file.metadata.contains_key("MIME-Version")); assert_eq!( parser.file.metadata.get("MIME-Version").unwrap(), "1.02.2", ); Ok(()) } #[test] fn parse_unescaped_double_quote() { // on keywords let path = "tests-data/unescaped-double-quote-msgid.po"; let mut parser = POFileParser::new(path.into()); let result = parser.parse(); assert_eq!( result, Err(SyntaxError::UnescapedDoubleQuoteFound { maybe_filename: MaybeFilename::new(path, true,), line: 5, index: 11, }) ); // on continuation lines let path = "tests-data/unescaped-double-quote-continuation.po"; let mut parser = POFileParser::new(path.into()); let result = parser.parse(); assert_eq!( result, Err(SyntaxError::UnescapedDoubleQuoteFound { maybe_filename: MaybeFilename::new(path, true,), line: 6, index: 27, }) ); } #[test] fn parse_obsolete_previous_msgid() -> Result<(), SyntaxError> { let path = "tests-data/obsolete-previous-msgid.po"; let mut parser = POFileParser::new(path.into()); parser.parse()?; assert_eq!(parser.file.entries.len(), 2); let obs_entry = &parser.file.entries[1]; assert_eq!(obs_entry.obsolete, true); assert!(obs_entry.previous_msgid.is_none()); assert!(obs_entry.fuzzy()); Ok(()) } #[test] fn error_when_empty_previous_message_line() { let content = concat!( "#\n", "msgid \"\"\n", "msgstr \"\"\n", "\n", "#|\n", "msgid \"foo\"\n", "msgstr \"bar\"\n", ); let mut parser = POFileParser::new(content.into()); let result = parser.parse(); assert_eq!( result, Err(SyntaxError::Custom { maybe_filename: MaybeFilename::new(content, false,), line: 5, index: 2, message: "empty previous message found".to_string(), }) ); } #[test] fn error_when_invalid_token_found() { let content = concat!( "#\n", "msgid \"\"\n", "msgstr \"\"\n", "\n", "#|msgid \"foo\"\n", "msgstr \"bar\"\n", ); let mut parser = POFileParser::new(content.into()); let result = parser.parse(); assert_eq!( result, Err(SyntaxError::Generic { maybe_filename: MaybeFilename::new(content, false,), line: 5, index: 0, }) ); } #[test] fn error_when_non_digit_msgstr_plural_index() { let content = concat!( "#\n", "msgid \"\"\n", "msgstr \"\"\n", "\n", "msgstr[foo] \"bar\"\n", ); let mut parser = POFileParser::new(content.into()); let result = parser.parse(); assert_eq!( result, Err(SyntaxError::Custom { maybe_filename: MaybeFilename::new(content, false,), line: 5, index: 7, message: "Invalid msgstr plural index. Expected digit, found 'foo'.".to_string(), }) ); } #[test] fn error_when_previous_msgid_invalid_continuation_line() { let path = "tests-data/invalid-previous-msgid-continuation.po"; let mut parser = POFileParser::new(path.into()); let result = parser.parse(); assert_eq!( result, Err(SyntaxError::Custom { maybe_filename: MaybeFilename::new(path, true), line: 4, index: 0, message: "invalid continuation line".to_string(), }) ); } #[test] fn error_when_unclosed_string_delimiter() { let path = "tests-data/unclosed-string-delimiter.po"; let mut parser = POFileParser::new(path.into()); let result = parser.parse(); assert_eq!( result, Err(SyntaxError::Custom { maybe_filename: MaybeFilename::new(path, true), line: 5, index: 12, message: "unterminated string '\"Foo bar bazá'" .to_string(), }) ) } } rspolib-0.1.1/src/prelude.rs000064400000000000000000000012661046102023000140770ustar 00000000000000//! rspolib prelude //! //! It includes traits to make use of the methods of files and entries. //! //! - [Save] trait to save a POFile or MOFile to a file using the `save` method. //! - [SaveAsMOFile] trait to use the method `save_as_mofile`. //! - [SaveAsPOFile] trait to use the method `save_as_pofile`. //! - [Merge] trait to use the method `merge`. //! - [TranslatedEntry] trait to use the method `translated` on entries. //! - [AsBytes] trait to use the methods `as_bytes*` on POFile and MOFile. //! - [MsgidEotMsgctxt] trait to use the method `msgid_eot_msgctxt` on entries. pub use crate::{ AsBytes, Merge, MsgidEotMsgctxt, Save, SaveAsMOFile, SaveAsPOFile, TranslatedEntry, }; rspolib-0.1.1/src/traits.rs000064400000000000000000000005201046102023000137350ustar 00000000000000/// Merge entries and files pub trait Merge { /// Merge a struct with another of the same type fn merge(&mut self, other: Self); } use std::io::{Read, Seek}; // Implementation to use `read_` and `seek_` methods // for different types of readers pub(crate) trait SeekRead: Seek + Read {} impl SeekRead for T {} rspolib-0.1.1/src/twrapper.rs000064400000000000000000000065721046102023000143100ustar 00000000000000use std::collections::HashMap; use unicode_linebreak::{ linebreaks as unicode_linebreaks, BreakOpportunity, }; use unicode_width::UnicodeWidthChar; #[allow(clippy::mut_range_bound)] fn get_linebreaks( linebreaks: &[(usize, BreakOpportunity)], text: &str, wrapwidth: usize, ) -> Vec { let char_indices_widths: HashMap = text .char_indices() .map(|(i, c)| (i, UnicodeWidthChar::width(c).unwrap_or(0))) .collect(); let mut ret = vec![]; let mut accum_char_bindex = 0; let mut accum_char_width = 0; // bindex, width let mut last_break_width = 0; for (lbi, (lb, _)) in linebreaks.iter().enumerate() { let range = accum_char_width..*lb; for bindex in range { accum_char_width += char_indices_widths.get(&bindex).unwrap_or(&0); accum_char_bindex = bindex; } if lbi == linebreaks.len() - 1 { continue; } let (next_lb, _) = linebreaks[lbi + 1]; let mut partial_accum_width = accum_char_width; for i in accum_char_bindex..next_lb { if let Some(width) = char_indices_widths.get(&i) { partial_accum_width += width; } } let width = partial_accum_width - last_break_width; if width > wrapwidth { ret.push(*lb); last_break_width = accum_char_width; } } ret } /// Wrap a text in lines using Unicode Line Breaking algorithm /// /// - `text` - Text to wrap in lines /// - `wrapwidth` - Maximum width of a line pub(crate) fn wrap(text: &str, wrapwidth: usize) -> Vec { let linebreaks = get_linebreaks( &unicode_linebreaks(text).collect::>(), text, wrapwidth, ); let mut ret: Vec = Vec::with_capacity(linebreaks.len() + 1); let mut prev_lb = 0; for lb in linebreaks { ret.push(text[prev_lb..lb].to_string()); prev_lb = lb; } ret.push(text[prev_lb..].to_string()); ret } #[cfg(test)] mod tests { use super::*; #[test] fn simple() { let text = "This is a test of the emergency broadcast system."; let wrapped = wrap(text, 10); assert_eq!( wrapped, vec![ "This is ", "a test ", "of the ", "emergency ", "broadcast ", "system." ] ); } #[test] fn long_wrapwidth() { let text = "This is a test of the emergency broadcast system."; let wrapped = wrap(text, 100); assert_eq!(wrapped, vec![text]); } #[test] fn unbreakable_line() { let text = "Thislineisverylongbutmustnotbebroken breaks should be here."; let wrapped = wrap(text, 5); assert_eq!( wrapped, vec![ "Thislineisverylongbutmustnotbebroken ", "breaks ", "should ", "be ", "here." ] ); } #[test] fn unicode_characters() { let text = "123Ááé aabbcc ÁáééÚí aabbcc"; let wrapped = wrap(text, 7); assert_eq!( wrapped, vec!["123Ááé ", "aabbcc ", "ÁáééÚí ", "aabbcc"] ); } } rspolib-0.1.1/tests-data/2-translated-entries.mo000064400000000000000000000001601046102023000176470ustar 000000000000004LLMU]^gmsgid 2msgid 4msgstr 2msgstr 4rspolib-0.1.1/tests-data/2-translated-entries.po000064400000000000000000000003241046102023000176540ustar 00000000000000# 2 translated and 3 untranslated entries msgid "" msgstr "" msgid "msgid 1" msgstr "" msgid "msgid 2" msgstr "msgstr 2" msgid "msgid 3" msgstr "" msgid "msgid 4" msgstr "msgstr 4" msgid "msgid 5" msgstr "" rspolib-0.1.1/tests-data/all-polib.mo000064400000000000000000000024601046102023000155600ustar 00000000000000\H v<gP=32FJywPEV=+%(field_label)s must be unique for %(date_field_label)s %(lookup_type)s.AfrikaansEnsure that there are no more than %(max)s digit in total.Ensure that there are no more than %(max)s digits in total.Ensure this value is %(limit_value)s (it is %(show_value)s).Enter a valid “slug” consisting of letters, numbers, underscores or hyphens.The number of days must be between {min_days} and {max_days}.abbrev. monthJan.Project-Id-Version: django Report-Msgid-Bugs-To: PO-Revision-Date: 2020-09-28 03:17+0000 Last-Translator: Foo Bar Language-Team: Spanish Language: es MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n != 1); %(field_label)s debe ser único para %(date_field_label)s %(lookup_type)s.AfricanoAsegúrese de que no hay más de %(max)s dígito en total.Asegúrese de que no haya más de %(max)s dígitos en total.Asegúrese de que este valor es %(limit_value)s (actualmente es %(show_value)s).Introduzca un 'slug' válido, consistente en letras, números, guiones bajos o medios.El número de días debe estar entre {min_days} y {max_days}.Ene.rspolib-0.1.1/tests-data/all.mo000064400000000000000000000025341046102023000144570ustar 00000000000000\ H v<P=!_2rJwPqV=W%(field_label)s must be unique for %(date_field_label)s %(lookup_type)s.AfrikaansEnsure that there are no more than %(max)s digit in total.Ensure that there are no more than %(max)s digits in total.Ensure this value is %(limit_value)s (it is %(show_value)s).Enter a valid “slug” consisting of letters, numbers, underscores or hyphens.The number of days must be between {min_days} and {max_days}.abbrev. monthJan.Project-Id-Version: django Report-Msgid-Bugs-To: PO-Revision-Date: 2020-09-28 03:17+0000 Last-Translator: Foo Bar Language-Team: Spanish Language: es MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n != 1); %(field_label)s debe ser único para %(date_field_label)s %(lookup_type)s.AfricanoAsegúrese de que no hay más de %(max)s dígito en total.Asegúrese de que no haya más de %(max)s dígitos en total.Asegúrese de que este valor es %(limit_value)s (actualmente es %(show_value)s).Introduzca un 'slug' válido, consistente en letras, números, guiones bajos o medios.El número de días debe estar entre {min_days} y {max_days}.Ene.rspolib-0.1.1/tests-data/all.po000064400000000000000000000045321046102023000144620ustar 00000000000000# This file is distributed under the same license as the Django package. # # Translators: # Qux Fox, 2015 # foobarbaz , 2018 # # Foo Baz, 2018 msgid "" msgstr "" "Project-Id-Version: django\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-05-19 20:23+0200\n" "PO-Revision-Date: 2020-09-28 03:17+0000\n" "Last-Translator: Foo Bar \n" "Language-Team: Spanish\n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" msgid "Afrikaans" msgstr "Africano" #. Translators: "letters" means latin letters: a-z and A-Z. msgid "" "Enter a valid “slug” consisting of letters, numbers, underscores or hyphens." msgstr "" "Introduzca un 'slug' válido, consistente en letras, números, guiones bajos o " "medios." #, python-format msgid "Ensure this value is %(limit_value)s (it is %(show_value)s)." msgstr "" "Asegúrese de que este valor es %(limit_value)s (actualmente es " "%(show_value)s)." #, python-format, fuzzy msgid "" "Ensure this value has at least %(limit_value)d character (it has " "%(show_value)d)." msgid_plural "" "Ensure this value has at least %(limit_value)d characters (it has " "%(show_value)d)." msgstr[0] "" "Asegúrese de que este valor tenga al menos %(limit_value)d caracter (tiene " "%(show_value)d)." msgstr[1] "" "Asegúrese de que este valor tenga al menos %(limit_value)d carácter(es) " "(tiene%(show_value)d)." #, python-format, foo-format, bar, baz msgid "Ensure that there are no more than %(max)s digit in total." msgid_plural "Ensure that there are no more than %(max)s digits in total." msgstr[0] "Asegúrese de que no hay más de %(max)s dígito en total." msgstr[1] "Asegúrese de que no haya más de %(max)s dígitos en total." #. Translators: The 'lookup_type' is one of 'date', 'year' or 'month'. #. Eg: "Title must be unique for pub_date year" #, python-format msgid "" "%(field_label)s must be unique for %(date_field_label)s %(lookup_type)s." msgstr "" "%(field_label)s debe ser único para %(date_field_label)s %(lookup_type)s." #, python-brace-format msgid "The number of days must be between {min_days} and {max_days}." msgstr "El número de días debe estar entre {min_days} y {max_days}." msgctxt "abbrev. month" msgid "Jan." msgstr "Ene." #~ msgid "hello 1" #~ msgstr "hola 1" rspolib-0.1.1/tests-data/comment-ordering.po000064400000000000000000000002051046102023000171540ustar 00000000000000msgid "" msgstr "" "Content-Type: text/plain; charset=UTF-8\n" # First comment line #. Second comment line msgid "foo" msgstr "oof" rspolib-0.1.1/tests-data/django-complete.po000064400000000000000000000723061046102023000167660ustar 00000000000000# This file is distributed under the same license as the Django package. # # Translators: # Foo Bar, 2013 # Foo Bar@gmail.com>, 2014 # Foo Bar, 2017 # Foo Bar , 2011-2014,2017,2019 # Foo Bar , 2020 # Foo Bar Baz , 2012 # Foo Bar , 2012 # Foo Bar, 2015-2016 # Foo Bar, 2014 # Foo Bar, 2020 # Foo Bar , 2017 # Foo Bar>, 2011 # Foo Bar , 2019 # Foo Bar , 2015 # Foo Bar <, 2011 # Foo Bar <, 2016 # Foo Bar Baz Qux <, 2014 # Foo Bar. Baz G. <,2013 # Foo Bar 2019 # Foo Bar , 2015 msgid "" msgstr "" "Project-Id-Version: django\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-05-19 20:23+0200\n" "PO-Revision-Date: 2020-09-28 03:17+0000\n" "Last-Translator: Uriel Medina \n" "Language-Team: Spanish (http://www.transifex.com/django/django/language/" "es/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: es\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" msgid "Afrikaans" msgstr "Africano" msgid "Arabic" msgstr "Árabe" msgid "Algerian Arabic" msgstr "Árabe argelino" msgid "Asturian" msgstr "Asturiano" msgid "Azerbaijani" msgstr "Azerbaiyán" msgid "Bulgarian" msgstr "Búlgaro" msgid "Belarusian" msgstr "Bielorruso" msgid "Bengali" msgstr "Bengalí" msgid "Breton" msgstr "Bretón" msgid "Bosnian" msgstr "Bosnio" msgid "Catalan" msgstr "Catalán" msgid "Czech" msgstr "Checo" msgid "Welsh" msgstr "Galés" msgid "Danish" msgstr "Danés" msgid "German" msgstr "Alemán" msgid "Lower Sorbian" msgstr "Bajo sorbio" msgid "Greek" msgstr "Griego" msgid "English" msgstr "Inglés" msgid "Australian English" msgstr "Inglés australiano" msgid "British English" msgstr "Inglés británico" msgid "Esperanto" msgstr "Esperanto" msgid "Spanish" msgstr "Español" msgid "Argentinian Spanish" msgstr "Español de Argentina" msgid "Colombian Spanish" msgstr "Español de Colombia" msgid "Mexican Spanish" msgstr "Español de México" msgid "Nicaraguan Spanish" msgstr "Español de Nicaragua" msgid "Venezuelan Spanish" msgstr "Español de Venezuela" msgid "Estonian" msgstr "Estonio" msgid "Basque" msgstr "Vasco" msgid "Persian" msgstr "Persa" msgid "Finnish" msgstr "Finés" msgid "French" msgstr "Francés" msgid "Frisian" msgstr "Frisón" msgid "Irish" msgstr "Irlandés" msgid "Scottish Gaelic" msgstr "Gaélico Escocés" msgid "Galician" msgstr "Gallego" msgid "Hebrew" msgstr "Hebreo" msgid "Hindi" msgstr "Hindi" msgid "Croatian" msgstr "Croata" msgid "Upper Sorbian" msgstr "Alto sorbio" msgid "Hungarian" msgstr "Húngaro" msgid "Armenian" msgstr "Armenio" msgid "Interlingua" msgstr "Interlingua" msgid "Indonesian" msgstr "Indonesio" msgid "Igbo" msgstr "Igbo" msgid "Ido" msgstr "Ido" msgid "Icelandic" msgstr "Islandés" msgid "Italian" msgstr "Italiano" msgid "Japanese" msgstr "Japonés" msgid "Georgian" msgstr "Georgiano" msgid "Kabyle" msgstr "Cabilio" msgid "Kazakh" msgstr "Kazajo" msgid "Khmer" msgstr "Khmer" msgid "Kannada" msgstr "Kannada" msgid "Korean" msgstr "Coreano" msgid "Kyrgyz" msgstr "Kirguís" msgid "Luxembourgish" msgstr "Luxenburgués" msgid "Lithuanian" msgstr "Lituano" msgid "Latvian" msgstr "Letón" msgid "Macedonian" msgstr "Macedonio" msgid "Malayalam" msgstr "Malayalam" msgid "Mongolian" msgstr "Mongol" msgid "Marathi" msgstr "Maratí" msgid "Burmese" msgstr "Birmano" msgid "Norwegian Bokmål" msgstr "Bokmål noruego" msgid "Nepali" msgstr "Nepalí" msgid "Dutch" msgstr "Holandés" msgid "Norwegian Nynorsk" msgstr "Nynorsk" msgid "Ossetic" msgstr "Osetio" msgid "Punjabi" msgstr "Panyabí" msgid "Polish" msgstr "Polaco" msgid "Portuguese" msgstr "Portugués" msgid "Brazilian Portuguese" msgstr "Portugués de Brasil" msgid "Romanian" msgstr "Rumano" msgid "Russian" msgstr "Ruso" msgid "Slovak" msgstr "Eslovaco" msgid "Slovenian" msgstr "Esloveno" msgid "Albanian" msgstr "Albanés" msgid "Serbian" msgstr "Serbio" msgid "Serbian Latin" msgstr "Serbio latino" msgid "Swedish" msgstr "Sueco" msgid "Swahili" msgstr "Suajili" msgid "Tamil" msgstr "Tamil" msgid "Telugu" msgstr "Telugu" msgid "Tajik" msgstr "Tayiko" msgid "Thai" msgstr "Tailandés" msgid "Turkmen" msgstr "Turcomanos" msgid "Turkish" msgstr "Turco" msgid "Tatar" msgstr "Tártaro" msgid "Udmurt" msgstr "Udmurt" msgid "Ukrainian" msgstr "Ucraniano" msgid "Urdu" msgstr "Urdu" msgid "Uzbek" msgstr "Uzbeko" msgid "Vietnamese" msgstr "Vietnamita" msgid "Simplified Chinese" msgstr "Chino simplificado" msgid "Traditional Chinese" msgstr "Chino tradicional" msgid "Messages" msgstr "Mensajes" msgid "Site Maps" msgstr "Mapas del sitio" msgid "Static Files" msgstr "Archivos estáticos" msgid "Syndication" msgstr "Sindicación" msgid "That page number is not an integer" msgstr "Este número de página no es un entero" msgid "That page number is less than 1" msgstr "Este número de página es menor que 1" msgid "That page contains no results" msgstr "Esa página no contiene resultados" msgid "Enter a valid value." msgstr "Introduzca un valor válido." msgid "Enter a valid URL." msgstr "Introduzca una URL válida." msgid "Enter a valid integer." msgstr "Introduzca un número entero válido." msgid "Enter a valid email address." msgstr "Introduzca una dirección de correo electrónico válida." #. Translators: "letters" means latin letters: a-z and A-Z. msgid "" "Enter a valid “slug” consisting of letters, numbers, underscores or hyphens." msgstr "" "Introduzca un 'slug' válido, consistente en letras, números, guiones bajos o " "medios." msgid "" "Enter a valid “slug” consisting of Unicode letters, numbers, underscores, or " "hyphens." msgstr "" "Introduzca un 'slug' válido, consistente en letras, números, guiones bajos o " "medios de Unicode." msgid "Enter a valid IPv4 address." msgstr "Introduzca una dirección IPv4 válida." msgid "Enter a valid IPv6 address." msgstr "Introduzca una dirección IPv6 válida." msgid "Enter a valid IPv4 or IPv6 address." msgstr "Introduzca una dirección IPv4 o IPv6 válida." msgid "Enter only digits separated by commas." msgstr "Introduzca sólo dígitos separados por comas." #, python-format msgid "Ensure this value is %(limit_value)s (it is %(show_value)s)." msgstr "" "Asegúrese de que este valor es %(limit_value)s (actualmente es " "%(show_value)s)." #, python-format msgid "Ensure this value is less than or equal to %(limit_value)s." msgstr "Asegúrese de que este valor es menor o igual a %(limit_value)s." #, python-format msgid "Ensure this value is greater than or equal to %(limit_value)s." msgstr "Asegúrese de que este valor es mayor o igual a %(limit_value)s." #, python-format msgid "" "Ensure this value has at least %(limit_value)d character (it has " "%(show_value)d)." msgid_plural "" "Ensure this value has at least %(limit_value)d characters (it has " "%(show_value)d)." msgstr[0] "" "Asegúrese de que este valor tenga al menos %(limit_value)d caracter (tiene " "%(show_value)d)." msgstr[1] "" "Asegúrese de que este valor tenga al menos %(limit_value)d carácter(es) " "(tiene%(show_value)d)." #, python-format msgid "" "Ensure this value has at most %(limit_value)d character (it has " "%(show_value)d)." msgid_plural "" "Ensure this value has at most %(limit_value)d characters (it has " "%(show_value)d)." msgstr[0] "" "Asegúrese de que este valor tenga menos de %(limit_value)d caracter (tiene " "%(show_value)d)." msgstr[1] "" "Asegúrese de que este valor tenga menos de %(limit_value)d caracteres (tiene " "%(show_value)d)." msgid "Enter a number." msgstr "Introduzca un número." #, python-format msgid "Ensure that there are no more than %(max)s digit in total." msgid_plural "Ensure that there are no more than %(max)s digits in total." msgstr[0] "Asegúrese de que no hay más de %(max)s dígito en total." msgstr[1] "Asegúrese de que no haya más de %(max)s dígitos en total." #, python-format msgid "Ensure that there are no more than %(max)s decimal place." msgid_plural "Ensure that there are no more than %(max)s decimal places." msgstr[0] "Asegúrese de que no haya más de %(max)s dígito decimal." msgstr[1] "Asegúrese de que no haya más de %(max)s dígitos decimales." #, python-format msgid "" "Ensure that there are no more than %(max)s digit before the decimal point." msgid_plural "" "Ensure that there are no more than %(max)s digits before the decimal point." msgstr[0] "" "Asegúrese de que no haya más de %(max)s dígito antes del punto decimal" msgstr[1] "" "Asegúrese de que no haya más de %(max)s dígitos antes del punto decimal." #, python-format msgid "" "File extension “%(extension)s” is not allowed. Allowed extensions are: " "%(allowed_extensions)s." msgstr "" "La extensión de archivo “%(extension)s” no esta permitida. Las extensiones " "permitidas son: %(allowed_extensions)s." msgid "Null characters are not allowed." msgstr "Los caracteres nulos no están permitidos." msgid "and" msgstr "y" #, python-format msgid "%(model_name)s with this %(field_labels)s already exists." msgstr "%(model_name)s con este %(field_labels)s ya existe." #, python-format msgid "Value %(value)r is not a valid choice." msgstr "Valor %(value)r no es una opción válida." msgid "This field cannot be null." msgstr "Este campo no puede ser nulo." msgid "This field cannot be blank." msgstr "Este campo no puede estar vacío." #, python-format msgid "%(model_name)s with this %(field_label)s already exists." msgstr "Ya existe %(model_name)s con este %(field_label)s." #. Translators: The 'lookup_type' is one of 'date', 'year' or 'month'. #. Eg: "Title must be unique for pub_date year" #, python-format msgid "" "%(field_label)s must be unique for %(date_field_label)s %(lookup_type)s." msgstr "" "%(field_label)s debe ser único para %(date_field_label)s %(lookup_type)s." #, python-format msgid "Field of type: %(field_type)s" msgstr "Campo de tipo: %(field_type)s" #, python-format msgid "“%(value)s” value must be either True or False." msgstr "“%(value)s”: el valor debe ser Verdadero o Falso." #, python-format msgid "“%(value)s” value must be either True, False, or None." msgstr "“%(value)s”: el valor debe ser Verdadero, Falso o Nulo." msgid "Boolean (Either True or False)" msgstr "Booleano (Verdadero o Falso)" #, python-format msgid "String (up to %(max_length)s)" msgstr "Cadena (máximo %(max_length)s)" msgid "Comma-separated integers" msgstr "Enteros separados por coma" #, python-format msgid "" "“%(value)s” value has an invalid date format. It must be in YYYY-MM-DD " "format." msgstr "" "“%(value)s” : el valor tiene un formato de fecha inválido. Debería estar en " "el formato YYYY-MM-DD." #, python-format msgid "" "“%(value)s” value has the correct format (YYYY-MM-DD) but it is an invalid " "date." msgstr "" "“%(value)s” : el valor tiene el formato correcto (YYYY-MM-DD) pero es una " "fecha inválida." msgid "Date (without time)" msgstr "Fecha (sin hora)" #, python-format msgid "" "“%(value)s” value has an invalid format. It must be in YYYY-MM-DD HH:MM[:ss[." "uuuuuu]][TZ] format." msgstr "" "“%(value)s”: el valor tiene un formato inválido. Debería estar en el formato " "YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]." #, python-format msgid "" "“%(value)s” value has the correct format (YYYY-MM-DD HH:MM[:ss[.uuuuuu]]" "[TZ]) but it is an invalid date/time." msgstr "" "“%(value)s”: el valor tiene el formato correcto (YYYY-MM-DD HH:MM[:ss[." "uuuuuu]][TZ]) pero es una fecha inválida." msgid "Date (with time)" msgstr "Fecha (con hora)" #, python-format msgid "“%(value)s” value must be a decimal number." msgstr "“%(value)s”: el valor debe ser un número decimal." msgid "Decimal number" msgstr "Número decimal" #, python-format msgid "" "“%(value)s” value has an invalid format. It must be in [DD] [[HH:]MM:]ss[." "uuuuuu] format." msgstr "" "“%(value)s”: el valor tiene un formato inválido. Debería estar en el formato " "[DD] [[HH:]MM:]ss[.uuuuuu]" msgid "Duration" msgstr "Duración" msgid "Email address" msgstr "Correo electrónico" msgid "File path" msgstr "Ruta de fichero" #, python-format msgid "“%(value)s” value must be a float." msgstr "“%(value)s”: el valor debería ser un número de coma flotante." msgid "Floating point number" msgstr "Número en coma flotante" #, python-format msgid "“%(value)s” value must be an integer." msgstr "“%(value)s”: el valor debería ser un numero entero" msgid "Integer" msgstr "Entero" msgid "Big (8 byte) integer" msgstr "Entero grande (8 bytes)" msgid "IPv4 address" msgstr "Dirección IPv4" msgid "IP address" msgstr "Dirección IP" #, python-format msgid "“%(value)s” value must be either None, True or False." msgstr "“%(value)s”: el valor debería ser None, Verdadero o Falso." msgid "Boolean (Either True, False or None)" msgstr "Booleano (Verdadero, Falso o Nulo)" msgid "Positive big integer" msgstr "Entero grande positivo" msgid "Positive integer" msgstr "Entero positivo" msgid "Positive small integer" msgstr "Entero positivo corto" #, python-format msgid "Slug (up to %(max_length)s)" msgstr "Slug (hasta %(max_length)s)" msgid "Small integer" msgstr "Entero corto" msgid "Text" msgstr "Texto" #, python-format msgid "" "“%(value)s” value has an invalid format. It must be in HH:MM[:ss[.uuuuuu]] " "format." msgstr "" "“%(value)s”: el valor tiene un formato inválido. Debería estar en el formato " "HH:MM[:ss[.uuuuuu]]." #, python-format msgid "" "“%(value)s” value has the correct format (HH:MM[:ss[.uuuuuu]]) but it is an " "invalid time." msgstr "" "“%(value)s” : el valor tiene el formato correcto (HH:MM[:ss[.uuuuuu]]) pero " "es un tiempo inválido." msgid "Time" msgstr "Hora" msgid "URL" msgstr "URL" msgid "Raw binary data" msgstr "Datos binarios en bruto" #, python-format msgid "“%(value)s” is not a valid UUID." msgstr "“%(value)s” no es un UUID válido." msgid "Universally unique identifier" msgstr "Identificador universal único" msgid "File" msgstr "Archivo" msgid "Image" msgstr "Imagen" msgid "A JSON object" msgstr "Un objeto JSON" msgid "Value must be valid JSON." msgstr "El valor debe ser un objeto JSON válido." #, python-format msgid "%(model)s instance with %(field)s %(value)r does not exist." msgstr "La instancia de %(model)s con %(field)s %(value)r no existe." msgid "Foreign Key (type determined by related field)" msgstr "Clave foránea (tipo determinado por el campo relacionado)" msgid "One-to-one relationship" msgstr "Relación uno-a-uno" #, python-format msgid "%(from)s-%(to)s relationship" msgstr "relación %(from)s-%(to)s" #, python-format msgid "%(from)s-%(to)s relationships" msgstr "relaciones %(from)s-%(to)s" msgid "Many-to-many relationship" msgstr "Relación muchos-a-muchos" #. Translators: If found as last label character, these punctuation #. characters will prevent the default label_suffix to be appended to the #. label msgid ":?.!" msgstr ":?.!" msgid "This field is required." msgstr "Este campo es obligatorio." msgid "Enter a whole number." msgstr "Introduzca un número entero." msgid "Enter a valid date." msgstr "Introduzca una fecha válida." msgid "Enter a valid time." msgstr "Introduzca una hora válida." msgid "Enter a valid date/time." msgstr "Introduzca una fecha/hora válida." msgid "Enter a valid duration." msgstr "Introduzca una duración válida." #, python-brace-format msgid "The number of days must be between {min_days} and {max_days}." msgstr "El número de días debe estar entre {min_days} y {max_days}." msgid "No file was submitted. Check the encoding type on the form." msgstr "" "No se ha enviado ningún fichero. Compruebe el tipo de codificación en el " "formulario." msgid "No file was submitted." msgstr "No se ha enviado ningún fichero" msgid "The submitted file is empty." msgstr "El fichero enviado está vacío." #, python-format msgid "Ensure this filename has at most %(max)d character (it has %(length)d)." msgid_plural "" "Ensure this filename has at most %(max)d characters (it has %(length)d)." msgstr[0] "" "Asegúrese de que este nombre de archivo tenga como máximo %(max)d caracter " "(tiene %(length)d)." msgstr[1] "" "Asegúrese de que este nombre de archivo tenga como máximo %(max)d " "carácter(es) (tiene %(length)d)." msgid "Please either submit a file or check the clear checkbox, not both." msgstr "" "Por favor envíe un fichero o marque la casilla de limpiar, pero no ambos." msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "" "Envíe una imagen válida. El fichero que ha enviado no era una imagen o se " "trataba de una imagen corrupta." #, python-format msgid "Select a valid choice. %(value)s is not one of the available choices." msgstr "" "Escoja una opción válida. %(value)s no es una de las opciones disponibles." msgid "Enter a list of values." msgstr "Introduzca una lista de valores." msgid "Enter a complete value." msgstr "Introduzca un valor completo." msgid "Enter a valid UUID." msgstr "Introduzca un UUID válido." msgid "Enter a valid JSON." msgstr "Ingresa un JSON válido." #. Translators: This is the default suffix added to form field labels msgid ":" msgstr ":" #, python-format msgid "(Hidden field %(name)s) %(error)s" msgstr "(Campo oculto %(name)s) *%(error)s" msgid "ManagementForm data is missing or has been tampered with" msgstr "Los datos de ManagementForm faltan o han sido manipulados" #, python-format msgid "Please submit %d or fewer forms." msgid_plural "Please submit %d or fewer forms." msgstr[0] "Por favor, envíe %d formulario o menos." msgstr[1] "Por favor, envíe %d formularios o menos" #, python-format msgid "Please submit %d or more forms." msgid_plural "Please submit %d or more forms." msgstr[0] "Por favor, envíe %d formulario o más." msgstr[1] "Por favor, envíe %d formularios o más." msgid "Order" msgstr "Orden" msgid "Delete" msgstr "Eliminar" #, python-format msgid "Please correct the duplicate data for %(field)s." msgstr "Por favor, corrija el dato duplicado para %(field)s." #, python-format msgid "Please correct the duplicate data for %(field)s, which must be unique." msgstr "" "Por favor corrija el dato duplicado para %(field)s, ya que debe ser único." #, python-format msgid "" "Please correct the duplicate data for %(field_name)s which must be unique " "for the %(lookup)s in %(date_field)s." msgstr "" "Por favor corrija los datos duplicados para %(field_name)s ya que debe ser " "único para %(lookup)s en %(date_field)s." msgid "Please correct the duplicate values below." msgstr "Por favor, corrija los valores duplicados abajo." msgid "The inline value did not match the parent instance." msgstr "El valor en línea no coincide con la instancia padre." msgid "Select a valid choice. That choice is not one of the available choices." msgstr "Escoja una opción válida. Esa opción no está entre las disponibles." #, python-format msgid "“%(pk)s” is not a valid value." msgstr "“%(pk)s” no es un valor válido." #, python-format msgid "" "%(datetime)s couldn’t be interpreted in time zone %(current_timezone)s; it " "may be ambiguous or it may not exist." msgstr "" "%(datetime)s no pudo ser interpretado en la zona horaria " "%(current_timezone)s; podría ser ambiguo o no existir." msgid "Clear" msgstr "Limpiar" msgid "Currently" msgstr "Actualmente" msgid "Change" msgstr "Modificar" msgid "Unknown" msgstr "Desconocido" msgid "Yes" msgstr "Sí" msgid "No" msgstr "No" #. Translators: Please do not add spaces around commas. msgid "yes,no,maybe" msgstr "sí,no,quizás" #, python-format msgid "%(size)d byte" msgid_plural "%(size)d bytes" msgstr[0] "%(size)d byte" msgstr[1] "%(size)d bytes" #, python-format msgid "%s KB" msgstr "%s KB" #, python-format msgid "%s MB" msgstr "%s MB" #, python-format msgid "%s GB" msgstr "%s GB" #, python-format msgid "%s TB" msgstr "%s TB" #, python-format msgid "%s PB" msgstr "%s PB" msgid "p.m." msgstr "p.m." msgid "a.m." msgstr "a.m." msgid "PM" msgstr "PM" msgid "AM" msgstr "AM" msgid "midnight" msgstr "medianoche" msgid "noon" msgstr "mediodía" msgid "Monday" msgstr "Lunes" msgid "Tuesday" msgstr "Martes" msgid "Wednesday" msgstr "Miércoles" msgid "Thursday" msgstr "Jueves" msgid "Friday" msgstr "Viernes" msgid "Saturday" msgstr "Sábado" msgid "Sunday" msgstr "Domingo" msgid "Mon" msgstr "Lun" msgid "Tue" msgstr "Mar" msgid "Wed" msgstr "Mié" msgid "Thu" msgstr "Jue" msgid "Fri" msgstr "Vie" msgid "Sat" msgstr "Sáb" msgid "Sun" msgstr "Dom" msgid "January" msgstr "Enero" msgid "February" msgstr "Febrero" msgid "March" msgstr "Marzo" msgid "April" msgstr "Abril" msgid "May" msgstr "Mayo" msgid "June" msgstr "Junio" msgid "July" msgstr "Julio" msgid "August" msgstr "Agosto" msgid "September" msgstr "Septiembre" msgid "October" msgstr "Octubre" msgid "November" msgstr "Noviembre" msgid "December" msgstr "Diciembre" msgid "jan" msgstr "ene" msgid "feb" msgstr "feb" msgid "mar" msgstr "mar" msgid "apr" msgstr "abr" msgid "may" msgstr "may" msgid "jun" msgstr "jun" msgid "jul" msgstr "jul" msgid "aug" msgstr "ago" msgid "sep" msgstr "sep" msgid "oct" msgstr "oct" msgid "nov" msgstr "nov" msgid "dec" msgstr "dic" msgctxt "abbrev. month" msgid "Jan." msgstr "Ene." msgctxt "abbrev. month" msgid "Feb." msgstr "Feb." msgctxt "abbrev. month" msgid "March" msgstr "Mar." msgctxt "abbrev. month" msgid "April" msgstr "Abr." msgctxt "abbrev. month" msgid "May" msgstr "Mayo" msgctxt "abbrev. month" msgid "June" msgstr "Jun." msgctxt "abbrev. month" msgid "July" msgstr "Jul." msgctxt "abbrev. month" msgid "Aug." msgstr "Ago." msgctxt "abbrev. month" msgid "Sept." msgstr "Sept." msgctxt "abbrev. month" msgid "Oct." msgstr "Oct." msgctxt "abbrev. month" msgid "Nov." msgstr "Nov." msgctxt "abbrev. month" msgid "Dec." msgstr "Dic." msgctxt "alt. month" msgid "January" msgstr "Enero" msgctxt "alt. month" msgid "February" msgstr "Febrero" msgctxt "alt. month" msgid "March" msgstr "Marzo" msgctxt "alt. month" msgid "April" msgstr "Abril" msgctxt "alt. month" msgid "May" msgstr "Mayo" msgctxt "alt. month" msgid "June" msgstr "Junio" msgctxt "alt. month" msgid "July" msgstr "Julio" msgctxt "alt. month" msgid "August" msgstr "Agosto" msgctxt "alt. month" msgid "September" msgstr "Septiembre" msgctxt "alt. month" msgid "October" msgstr "Octubre" msgctxt "alt. month" msgid "November" msgstr "Noviembre" msgctxt "alt. month" msgid "December" msgstr "Diciembre" msgid "This is not a valid IPv6 address." msgstr "No es una dirección IPv6 válida." #, python-format msgctxt "String to return when truncating text" msgid "%(truncated_text)s…" msgstr "%(truncated_text)s..." msgid "or" msgstr "o" #. Translators: This string is used as a separator between list elements msgid ", " msgstr ", " #, python-format msgid "%d year" msgid_plural "%d years" msgstr[0] "%d año" msgstr[1] "%d años" #, python-format msgid "%d month" msgid_plural "%d months" msgstr[0] "%d mes" msgstr[1] "%d meses" #, python-format msgid "%d week" msgid_plural "%d weeks" msgstr[0] "%d semana" msgstr[1] "%d semanas" #, python-format msgid "%d day" msgid_plural "%d days" msgstr[0] "%d día" msgstr[1] "%d días" #, python-format msgid "%d hour" msgid_plural "%d hours" msgstr[0] "%d hora" msgstr[1] "%d horas" #, python-format msgid "%d minute" msgid_plural "%d minutes" msgstr[0] "%d minuto" msgstr[1] "%d minutos" msgid "Forbidden" msgstr "Prohibido" msgid "CSRF verification failed. Request aborted." msgstr "La verificación CSRF ha fallado. Solicitud abortada." msgid "" "You are seeing this message because this HTTPS site requires a “Referer " "header” to be sent by your Web browser, but none was sent. This header is " "required for security reasons, to ensure that your browser is not being " "hijacked by third parties." msgstr "" "Está viendo este mensaje porque este sitio HTTPS requiere que su navegador " "web envíe un \"encabezado de referencia\", pero no se envió ninguno. Este " "encabezado es necesario por razones de seguridad, para garantizar que su " "navegador no sea secuestrado por terceros." msgid "" "If you have configured your browser to disable “Referer” headers, please re-" "enable them, at least for this site, or for HTTPS connections, or for “same-" "origin” requests." msgstr "" "Si ha configurado su navegador para deshabilitar los encabezados \"Referer" "\", vuelva a habilitarlos, al menos para este sitio, o para conexiones " "HTTPS, o para solicitudes del \"mismo origen\"." msgid "" "If you are using the tag or " "including the “Referrer-Policy: no-referrer” header, please remove them. The " "CSRF protection requires the “Referer” header to do strict referer checking. " "If you’re concerned about privacy, use alternatives like for links to third-party sites." msgstr "" "Si esta utilizando la etiqueta o incluyendo el encabezado \"Referrer-Policy: no-referrer\", elimínelos. " "La protección CSRF requiere que el encabezado \"Referer\" realice una " "comprobación estricta del referente. Si le preocupa la privacidad, utilice " "alternativas como para los enlaces a sitios de " "terceros." msgid "" "You are seeing this message because this site requires a CSRF cookie when " "submitting forms. This cookie is required for security reasons, to ensure " "that your browser is not being hijacked by third parties." msgstr "" "Estás viendo este mensaje porqué esta web requiere una cookie CSRF cuando se " "envían formularios. Esta cookie se necesita por razones de seguridad, para " "asegurar que tu navegador no ha sido comprometido por terceras partes." msgid "" "If you have configured your browser to disable cookies, please re-enable " "them, at least for this site, or for “same-origin” requests." msgstr "" "Si ha configurado su navegador para deshabilitar las cookies, vuelva a " "habilitarlas, al menos para este sitio o para solicitudes del \"mismo origen" "\"." msgid "More information is available with DEBUG=True." msgstr "Más información disponible si se establece DEBUG=True." msgid "No year specified" msgstr "No se ha indicado el año" msgid "Date out of range" msgstr "Fecha fuera de rango" msgid "No month specified" msgstr "No se ha indicado el mes" msgid "No day specified" msgstr "No se ha indicado el día" msgid "No week specified" msgstr "No se ha indicado la semana" #, python-format msgid "No %(verbose_name_plural)s available" msgstr "No %(verbose_name_plural)s disponibles" #, python-format msgid "" "Future %(verbose_name_plural)s not available because %(class_name)s." "allow_future is False." msgstr "" "Los futuros %(verbose_name_plural)s no están disponibles porque " "%(class_name)s.allow_future es Falso." #, python-format msgid "Invalid date string “%(datestr)s” given format “%(format)s”" msgstr "Cadena de fecha no valida “%(datestr)s” dado el formato “%(format)s”" #, python-format msgid "No %(verbose_name)s found matching the query" msgstr "No se encontró ningún %(verbose_name)s coincidente con la consulta" msgid "Page is not “last”, nor can it be converted to an int." msgstr "La página no es la \"última\", ni se puede convertir a un entero." #, python-format msgid "Invalid page (%(page_number)s): %(message)s" msgstr "Página inválida (%(page_number)s): %(message)s" #, python-format msgid "Empty list and “%(class_name)s.allow_empty” is False." msgstr "Lista vacía y “%(class_name)s.allow_empty” es Falso" msgid "Directory indexes are not allowed here." msgstr "Los índices de directorio no están permitidos." #, python-format msgid "“%(path)s” does not exist" msgstr "“%(path)s” no existe" #, python-format msgid "Index of %(directory)s" msgstr "Índice de %(directory)s" msgid "Django: the Web framework for perfectionists with deadlines." msgstr "Django: el marco web para perfeccionistas con plazos." #, python-format msgid "" "View release notes for Django %(version)s" msgstr "" "Ve la notas de la versión de Django " "%(version)s" msgid "The install worked successfully! Congratulations!" msgstr "¡La instalación funcionó con éxito! ¡Felicitaciones!" #, python-format msgid "" "You are seeing this page because DEBUG=True is in your settings file and you have not configured any " "URLs." msgstr "" "Estás viendo esta página porque DEBUG=True está en su archivo de configuración y no ha configurado " "ninguna URL." msgid "Django Documentation" msgstr "Documentación de Django" msgid "Topics, references, & how-to’s" msgstr "Temas, referencias, & como hacer" msgid "Tutorial: A Polling App" msgstr "Tutorial: Una aplicación de encuesta" msgid "Get started with Django" msgstr "Comienza con Django" msgid "Django Community" msgstr "Comunidad Django" #~ msgid "hello 1" #~ msgstr "hola 1" msgid "Connect, get help, or contribute" msgstr "Conéctate, obtén ayuda o contribuye" #~ msgid "hello 2" #~ msgstr "hola 2" rspolib-0.1.1/tests-data/docs/.gitignore000064400000000000000000000000251046102023000162630ustar 00000000000000* !.gitignore !.keep rspolib-0.1.1/tests-data/docs/.keep000064400000000000000000000000001046102023000152110ustar 00000000000000rspolib-0.1.1/tests-data/empty-metadata.mo000064400000000000000000000000561046102023000166200ustar 00000000000000$,,-rspolib-0.1.1/tests-data/empty-metadata.po000064400000000000000000000000231046102023000166150ustar 00000000000000msgid "" msgstr "" rspolib-0.1.1/tests-data/empty-occurrences-line.po000064400000000000000000000002701046102023000203010ustar 00000000000000#: db/models/manipulators.py:310 contrib/admin/views/main.py:342 #: contrib/admin/views/main.py:344 contrib/admin/views/main.py:346 #: core/validators.py:275 #: msgid "and" msgstr "y" rspolib-0.1.1/tests-data/empty.mo000064400000000000000000000000001046102023000150270ustar 00000000000000rspolib-0.1.1/tests-data/empty.po000064400000000000000000000000001046102023000150320ustar 00000000000000rspolib-0.1.1/tests-data/escapes.po000064400000000000000000000001451046102023000153310ustar 00000000000000# msgid "" msgstr "" msgid "\\ \t \r \b \n \\\n \v \f \\\\" msgstr "\\ \t \r \b \n \\\n \v \f \\\\" rspolib-0.1.1/tests-data/flags.po000064400000000000000000000005021046102023000147770ustar 00000000000000#, python-format, fuzzy msgid "msgid 1" msgstr "msgstr 1" #, fuzzy msgid "msgid 2" msgstr "msgstr 2" #, python-format msgid "msgid 3" msgstr "msgstr 3" #, 1, 2, 3, 4, 5, 6, 7 msgid "msgid 4" msgstr "msgstr 4" #, #, #, a #, b #, c, d, e #, f #, #, g msgid "msgid 5" msgstr "msgstr 5" msgid "msgid 6" msgstr "msgstr 6" rspolib-0.1.1/tests-data/fuzzy-header.po000064400000000000000000000007661046102023000163340ustar 00000000000000# Po file with # a fuzzy header # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2010-02-08 16:57+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-SearchPath-1: Foo\n" "X-Poedit-SearchPath-2: Bar\n" "X-Poedit-SearchPath-10: Baz\n" rspolib-0.1.1/tests-data/fuzzy-no-fuzzy.po000064400000000000000000000002421046102023000166720ustar 00000000000000# test file for setting fuzzy messages msgid "" msgstr "" "Content-Type: text/plain; charset=UTF-8\n" msgid "a" msgstr "a" #, fuzzy msgid "Line" msgstr "Ligne" rspolib-0.1.1/tests-data/header-no-trailing-newline.po000064400000000000000000000001411046102023000210120ustar 00000000000000# This file is distributed under the same license # # Translators: # Foo bar, Year # # Baz, YEARrspolib-0.1.1/tests-data/header-trailing-newlines.po000064400000000000000000000001501046102023000205630ustar 00000000000000# This file is distributed under the same license # # Translators: # Foo bar, Year # # Baz, YEAR # # # rspolib-0.1.1/tests-data/indented.po000064400000000000000000000015331046102023000155020ustar 00000000000000# msgid "" msgstr "" # Added for previous msgid/msgid_plural/msgctxt testing # Tokens are separated by some tabs and a single space. #| msgctxt "@previous_context" #| msgid "previous untranslated entry" #| msgid_plural "previous untranslated entry plural" msgctxt "@context" msgid "Some msgid" msgstr "Some msgstr" # Same thing with plurals. # Each keyword is followed by some tabs and a single space. #, python-format msgid "" "Please enter valid %(self)s IDs. " "The value %(value)r is invalid." msgid_plural "" "Please enter valid %(self)s IDs. " "The values %(value)r are invalid." msgstr[0] "" "Por favor, introduzca IDs de %(self)s válidos. " "El valor %(value)r no es válido." msgstr[1] "" "Por favor, introduzca IDs de %(self)s válidos. " "Los valores %(value)r no son válidos." rspolib-0.1.1/tests-data/invalid-previous-msgid-continuation.po000064400000000000000000000001111046102023000230100ustar 00000000000000msgid "FOO" msgstr "BAR" #| msgid "Foo bar baz" msgstr "" "Foo bar baz" rspolib-0.1.1/tests-data/invalid-version-number.mo000064400000000000000000000000101046102023000202710ustar 00000000000000rspolib-0.1.1/tests-data/long-message.po000064400000000000000000000003531046102023000162700ustar 00000000000000#. This is a generated/extracted comment msgid "" "Enter a valid “slug” consisting of letters, numbers, underscores or hyphens." msgstr "" "Introduzca un 'slug' válido, consistente en letras, números, guiones bajos o " "medios." rspolib-0.1.1/tests-data/metadata.po000064400000000000000000000007241046102023000154710ustar 00000000000000msgid "" msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "POT-Creation-Date: 2020-05-19 20:23+0200\n" "Content-Transfer-Encoding: 8bit\n" "MIME-Version: 1.0\n" "Report-Msgid-Bugs-To: mondeja\n" "PO-Revision-Date: 2020-09-28 03:17+0000\n" "Project-Id-Version: django\n" "Last-Translator: Foo Bar \n" "Content-Type: text/plain; charset=UTF-8\n" "Language: es\n" "Language-Team: Spanish (http://www.transifex.com/django/django/language/es/)\n" rspolib-0.1.1/tests-data/msgctxt.po000064400000000000000000000002471046102023000154020ustar 00000000000000msgctxt "abbrev. month" msgid "Jan." msgstr "Ene." #, fuzzy msgctxt "abbrev. month" msgid "J." msgstr "E." msgctxt "to date" msgid "To date" msgstr "Hasta la fecha" rspolib-0.1.1/tests-data/msgid-msgstr-long.po000064400000000000000000000021241046102023000172620ustar 00000000000000msgid "msgid 1 msgid 1 msgid 1 msgid 1 msgid 1 msgid 1 msgid 1 msgid 1 msgid 1 msgid 1 msgid 1 msgid 1" msgstr "msgstr 1 msgstr 1 msgstr 1 msgstr 1 msgstr 1 msgstr 1 msgstr 1 msgstr 1 msgstr 1 msgstr 1 msgstr 1" msgid "" "\n" "

To install bookmarklets, drag the link to your bookmarks\n" "toolbar, or right-click the link and add it to your bookmarks. Now you can\n" "select the bookmarklet from any page in the site. Note that some of these\n" "bookmarklets require you to be viewing the site from a computer designated\n" "as \"internal\" (talk to your system administrator if you aren't sure if\n" "your computer is \"internal\").

\n" msgstr "" "\n" "

To install bookmarklets, drag the link to your bookmarks\n" "toolbar, or right-click the link and add it to your bookmarks. Now you can\n" "select the bookmarklet from any page in the site. Note that some of these\n" "bookmarklets require you to be viewing the site from a computer designated\n" "as \"internal\" (talk to your system administrator if you aren't sure if\n" "your computer is \"internal\").

\n" rspolib-0.1.1/tests-data/msgid-msgstr.po000064400000000000000000000000421046102023000163220ustar 00000000000000msgid "msgid 1" msgstr "msgstr 1" rspolib-0.1.1/tests-data/msgid-plural.po000064400000000000000000000015451046102023000163130ustar 00000000000000msgid "" "A Ensure this value has at least %(limit_value)d character (it has " "%(show_value)d)." msgid_plural "" "A Ensure this value has at least %(limit_value)d characters (it has " "%(show_value)d)." msgstr[0] "" "A Asegúrese de que este valor tenga al menos %(limit_value)d caracter (tiene " "%(show_value)d)." msgstr[1] "" "A Asegúrese de que este valor tenga al menos %(limit_value)d carácter(es) " "(tiene%(show_value)d)." msgid "" "B Ensure this value has at least %(limit_value)d character (it has " "%(show_value)d)." msgid_plural "" "B Ensure this value has at least %(limit_value)d characters (it has " "%(show_value)d)." msgstr[5] "" "B Asegúrese de que este valor tenga al menos %(limit_value)d caracter (tiene " "%(show_value)d)." msgstr[3] "" "B Asegúrese de que este valor tenga al menos %(limit_value)d carácter(es) " "(tiene%(show_value)d)." rspolib-0.1.1/tests-data/msgids-msgstrs.po000064400000000000000000000001331046102023000166710ustar 00000000000000# msgid "" msgstr "" msgid "msgid 1" msgstr "msgstr 1" msgid "msgid 2" msgstr "msgstr 2" rspolib-0.1.1/tests-data/natural-unsorted-metadata.po000064400000000000000000000004541046102023000207760ustar 00000000000000msgid "" msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Report-Msgid-Bugs-To: \n" "X-Poedit-SearchPath-10: Baz\n" "Project-Id-Version: PACKAGE VERSION\n" "X-Poedit-SearchPath-2: Bar\n" "X-Poedit-SearchPath-1: Foo\n" "Language-Team: LANGUAGE \n" rspolib-0.1.1/tests-data/obsolete-previous-msgid.po000064400000000000000000000005021046102023000204720ustar 00000000000000# msgid "" msgstr "" #: ../addressbook/addressbook.error.xml.h:1 msgid "This address book could not be opened." msgstr "No s'ha pogut obrir aquesta llibreta d'adreces." #, fuzzy #~| msgid "" #~| "Error on %s\n" #~| "%s" #~ msgid "" #~ "Error on %s: %s\n" #~ "%s" #~ msgstr "" #~ "S'ha produït un error en %s:\n" #~ "%s"rspolib-0.1.1/tests-data/obsoletes.po000064400000000000000000000002041046102023000157010ustar 00000000000000# msgid "" msgstr "" msgid "hello 1" msgstr "hola 1" #~ msgid "hello 2" #~ msgstr "hola 2" #~ msgid "hello 3" #~ msgstr "hola 3" rspolib-0.1.1/tests-data/occurrence-no-linenum.po000064400000000000000000000001171046102023000201140ustar 00000000000000#: path/to/file/noocc.rs path/to/file/occ.rs:45 msgid "Hello" msgstr "Bonjour" rspolib-0.1.1/tests-data/previous-msgid-continuation.po000064400000000000000000000001401046102023000213660ustar 00000000000000msgid "FOO" msgstr "BAR" #| msgid "" #| "Bar baz qux" msgid "Foo bar baz" msgstr "Foo bar baz" rspolib-0.1.1/tests-data/previous-msgid-msgctxt.po000064400000000000000000000002151046102023000203500ustar 00000000000000# msgid "" msgstr "" #| msgctxt "@previous_context" #| msgid "previous untranslated entry" msgctxt "@context5" msgid "Some msgid" msgstr "" rspolib-0.1.1/tests-data/repeated-metadata-keys.po000064400000000000000000000002661046102023000202320ustar 00000000000000msgid "" msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 4bit\n" "MIME-Version: 1.0\n" "MIME-Version: 2.2\n" rspolib-0.1.1/tests-data/tests/.gitignore000064400000000000000000000000251046102023000164750ustar 00000000000000* !.gitignore !.keep rspolib-0.1.1/tests-data/tests/.keep000064400000000000000000000000001046102023000154230ustar 00000000000000rspolib-0.1.1/tests-data/unclosed-string-delimiter.po000064400000000000000000000000531046102023000210000ustar 00000000000000# msgid "" msgstr "" msgid "Foo bar bazá rspolib-0.1.1/tests-data/unescaped-double-quote-continuation.po000064400000000000000000000001701046102023000227660ustar 00000000000000# msgid "" msgstr "" msgid "" "foo bar baz qux fox quick" brown jumps foo bar baz qux fox quick brown jumps" msgstr "" rspolib-0.1.1/tests-data/unescaped-double-quote-msgid.po000064400000000000000000000000601046102023000213550ustar 00000000000000# msgid "" msgstr "" msgid "foo"bar" msgstr "" rspolib-0.1.1/tests-data/utf8-bom.po000064400000000000000000000000441046102023000153450ustar 00000000000000# This file contains an UTF8 BOM rspolib-0.1.1/tests-data/weird-occurrences.po000064400000000000000000000005171046102023000173340ustar 00000000000000# Windows paths #: C:\foo\bar.py:12 msgid "Windows path" msgstr "Windows path" #. Test for empty comment lines #. #, #: main.c:117 msgid "Override the default prgname" msgstr "Override the default prgname" #: Balloon-Fills,BitmapFillStyle>>addFillStyleMenuItems:hand:from: msgid "choose new graphic" msgstr "escolher novo gráfico" rspolib-0.1.1/tests-data/wrapping.po000064400000000000000000000004751046102023000155430ustar 00000000000000# test wrapping msgid "" msgstr "" msgid "This line will not be wrapped" msgstr "" msgid "Some line that contain special characters \" and that \t is very, very, very long...: %s \n" msgstr "" msgid "Some line that contain special characters \"foobar\" and that contains whitespace at the end " msgstr ""