summaryrefslogtreecommitdiff
path: root/perl/Locale/Po4a/Text.pm
blob: 52360950e5841b482fca10322033bf2b1cea9076 (plain)
  1. #!/usr/bin/perl -w
  2. # Po4a::Text.pm
  3. #
  4. # extract and translate translatable strings from a text documents
  5. #
  6. # This program is free software; you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation; either version 2 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program; if not, write to the Free Software
  18. # Foundation, Inc.,
  19. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
  20. #
  21. ########################################################################
  22. =head1 NAME
  23. Locale::Po4a::Text - Convert text documents from/to PO files
  24. =head1 DESCRIPTION
  25. The po4a (po for anything) project goal is to ease translations (and more
  26. interestingly, the maintenance of translations) using gettext tools on
  27. areas where they were not expected like documentation.
  28. Locale::Po4a::Text is a module to help the translation of text documents into
  29. other [human] languages.
  30. Paragraphs are splitted on empty lines (or lines containing only spaces or
  31. tabulations).
  32. If a paragraph contains a line starting by a space (or tabulation), this
  33. paragraph won't be rewrapped.
  34. =cut
  35. package Locale::Po4a::Text;
  36. use 5.006;
  37. use strict;
  38. use warnings;
  39. require Exporter;
  40. use vars qw(@ISA @EXPORT);
  41. @ISA = qw(Locale::Po4a::TransTractor);
  42. @EXPORT = qw();
  43. use Locale::Po4a::TransTractor;
  44. use Locale::Po4a::Common;
  45. =head1 OPTIONS ACCEPTED BY THIS MODULE
  46. These are this module's particular options:
  47. =over
  48. =item B<nobullet>
  49. Deactivate detection of bullets.
  50. By default, when a bullet is detected, the bullet paragraph is not considered
  51. as a verbatim paragraph (with the no-wrap flag in the PO file), but the module
  52. rewrap this paragraph in the generated PO file and in the translation.
  53. =cut
  54. my $bullets = 1;
  55. =item B<debianchangelog>
  56. Handle the header and footer of
  57. released versions, which only contain non translatable informations.
  58. =cut
  59. my $debianchangelog = 0;
  60. =item B<fortunes>
  61. Handle the fortunes format, which separate fortunes with a line which
  62. consists in '%' or '%%', and use '%%' as the beginning of a comment.
  63. =cut
  64. my $fortunes = 0;
  65. =item B<markdown>
  66. Handle some special markup in Markdown-formatted texts.
  67. =cut
  68. my $markdown = 0;
  69. sub initialize {
  70. my $self = shift;
  71. my %options = @_;
  72. $self->{options}{'nobullets'}='';
  73. if (defined $options{'nobullets'}) {
  74. $bullets = 0;
  75. }
  76. if (defined $options{'debianchangelog'}) {
  77. $debianchangelog=1;
  78. }
  79. if (defined $options{'fortunes'}) {
  80. $fortunes=1;
  81. }
  82. if (defined $options{'markdown'}) {
  83. $markdown=1;
  84. }
  85. }
  86. sub parse {
  87. my $self = shift;
  88. my ($line,$ref);
  89. my $paragraph="";
  90. my $wrapped_mode = 1;
  91. my $expect_header = 1;
  92. my $end_of_paragraph = 0;
  93. ($line,$ref)=$self->shiftline();
  94. my $file = $ref;
  95. $file =~ s/:[0-9]+$//;
  96. while (defined($line)) {
  97. $ref =~ m/^(.*):[0-9]+$/;
  98. if ($1 ne $file) {
  99. $file = $1;
  100. do_paragraph($self,$paragraph,$wrapped_mode);
  101. $paragraph="";
  102. $wrapped_mode = 1;
  103. $expect_header = 1;
  104. }
  105. # TODO: preserve original line ends throughout the code instead
  106. chomp($line);
  107. $self->{ref}="$ref";
  108. if ($debianchangelog and
  109. $expect_header and
  110. $line =~ /^(\w[-+0-9a-z.]*)\ \(([^\(\) \t]+)\) # src, version
  111. \s+([-+0-9a-z.]+); # distribution
  112. \s*urgency\s*\=\s*(.*\S)\s*$/ix) { #
  113. do_paragraph($self,$paragraph,$wrapped_mode);
  114. $paragraph="";
  115. $self->pushline("$line\n");
  116. $expect_header=0;
  117. } elsif ($debianchangelog and
  118. $line =~ m/^ \-\- (.*) <(.*)> ((\w+\,\s*)?\d{1,2}\s+\w+\s+\d{4}\s+\d{1,2}:\d\d:\d\d\s+[-+]\d{4}(\s+\([^\\\(\)]\))?)$/) {
  119. # Found trailer
  120. do_paragraph($self,$paragraph,$wrapped_mode);
  121. $paragraph="";
  122. $self->pushline("$line\n");
  123. $expect_header=1;
  124. } elsif ($fortunes and
  125. $line =~ m/^%%?\s*$/) {
  126. # Found end of fortune
  127. do_paragraph($self,$paragraph,$wrapped_mode);
  128. # FIXME: test if this is still needed when always adding
  129. # newline in do_paragraph()
  130. $self->pushline("\n") unless ( $wrapped_mode == 0
  131. or $paragraph eq "");
  132. $paragraph="";
  133. $wrapped_mode = 1;
  134. $self->pushline("$line\n");
  135. } elsif ($line =~ /^\s*$/) {
  136. # Break paragraphs on lines containing only spaces
  137. do_paragraph($self,$paragraph,$wrapped_mode);
  138. $paragraph="";
  139. $wrapped_mode = 1;
  140. $self->pushline($line."\n");
  141. } elsif ( $line =~ /^=+$/
  142. or $line =~ /^_+$/
  143. or $line =~ /^-+$/) {
  144. $wrapped_mode = 0;
  145. $paragraph .= $line."\n";
  146. do_paragraph($self,$paragraph,$wrapped_mode);
  147. $paragraph="";
  148. $wrapped_mode = 1;
  149. } elsif ($markdown and
  150. ( $line =~ /^\s*\[\[\!\S+\s*$/ # macro begin
  151. or $line =~ /^\s*"""\s*\]\]\s*$/)) { # """ textblock inside macro end
  152. # Avoid translating Markdown lines containing only markup
  153. do_paragraph($self,$paragraph,$wrapped_mode);
  154. $paragraph="";
  155. $wrapped_mode = 1;
  156. $self->pushline("$line\n");
  157. } elsif ($markdown and
  158. ( $line =~ /^#/ # headline
  159. or $line =~ /^\s*\[\[\!\S[^\]]*\]\]\s*$/)) { # sole macro
  160. # Preserve some Markdown markup as a single line
  161. do_paragraph($self,$paragraph,$wrapped_mode);
  162. $paragraph="$line\n";
  163. $wrapped_mode = 0;
  164. $end_of_paragraph = 1;
  165. } elsif ($markdown and
  166. ( $line =~ /^"""/)) { # """ textblock inside macro end
  167. # Markdown markup needing separation _before_ this line
  168. do_paragraph($self,$paragraph,$wrapped_mode);
  169. $paragraph="$line\n";
  170. $wrapped_mode = 1;
  171. } else {
  172. if ($line =~ /^\s/) {
  173. # A line starting by a space indicates a non-wrap
  174. # paragraph
  175. $wrapped_mode = 0;
  176. } elsif ($markdown and
  177. ( $line =~ /\S $/ # explicit newline
  178. or $line =~ /"""$/)) { # """ textblock inside macro begin
  179. # Markdown markup needing separation _after_ this line
  180. $end_of_paragraph = 1;
  181. }
  182. if ($fortunes) {
  183. $line =~ s/%%(.*)$//;
  184. }
  185. # TODO: comments
  186. $paragraph .= $line."\n";
  187. }
  188. # paragraphs starting by a bullet, or numbered
  189. # or paragraphs with a line containing many consecutive spaces
  190. # (more than 3)
  191. # are considered as verbatim paragraphs
  192. $wrapped_mode = 0 if ( $paragraph =~ m/^(\*|[0-9]+[.)] )/s
  193. or $paragraph =~ m/[ \t][ \t][ \t]/s);
  194. if ($markdown) {
  195. # Some Markdown markup can (or might) not survive wrapping
  196. $wrapped_mode = 0 if (
  197. $paragraph =~ /^>/ms # blockquote
  198. or $paragraph =~ /^( {8}|\t)/ms # monospaced
  199. or $paragraph =~ /[<>]/ms # maybe html
  200. or $paragraph =~ /^\s*\[\[\!\S[^\]]+$/ms # macro begin
  201. );
  202. }
  203. if ($end_of_paragraph) {
  204. do_paragraph($self,$paragraph,$wrapped_mode);
  205. $paragraph="";
  206. $wrapped_mode = 1;
  207. $end_of_paragraph = 0;
  208. }
  209. ($line,$ref)=$self->shiftline();
  210. }
  211. if (length $paragraph) {
  212. do_paragraph($self,$paragraph,$wrapped_mode);
  213. }
  214. }
  215. sub do_paragraph {
  216. my ($self, $paragraph, $wrap) = (shift, shift, shift);
  217. return if ($paragraph eq "");
  218. if ($bullets) {
  219. # Detect bullets
  220. # | * blah blah
  221. # |<spaces> blah
  222. # | ^-- aligned
  223. # <empty line>
  224. #
  225. # Other bullets supported:
  226. # - blah o blah + blah
  227. # 1. blah 1) blah (1) blah
  228. TEST_BULLET:
  229. if ($paragraph =~ m/^(\s*)((?:[-*o+]|([0-9]+[.\)])|\([0-9]+\))\s+)([^\n]*\n)(.*)$/s) {
  230. my $para = $5;
  231. my $bullet = $2;
  232. my $indent1 = $1;
  233. my $indent2 = "$1".(' ' x length $bullet);
  234. my $text = $4;
  235. while ($para !~ m/$indent2(?:[-*o+]|([0-9]+[.\)])|\([0-9]+\))\s+/
  236. and $para =~ s/^$indent2(\S[^\n]*\n)//s) {
  237. $text .= $1;
  238. }
  239. # TODO: detect if a line starts with the same bullet
  240. if ($text !~ m/\S[ \t][ \t][ \t]+\S/s) {
  241. my $bullet_regex = quotemeta($indent1.$bullet);
  242. $bullet_regex =~ s/[0-9]+/\\d\+/;
  243. if ($para eq '' or $para =~ m/^$bullet_regex\S/s) {
  244. my $trans = $self->translate($text,
  245. $self->{ref},
  246. "Bullet: '$indent1$bullet'",
  247. "wrap" => 1,
  248. "wrapcol" => - (length $indent2));
  249. $trans =~ s/^/$indent1$bullet/s;
  250. $trans =~ s/\n(.)/\n$indent2$1/sg;
  251. $self->pushline( $trans."\n" );
  252. if ($para eq '') {
  253. return;
  254. } else {
  255. # Another bullet
  256. $paragraph = $para;
  257. goto TEST_BULLET;
  258. }
  259. }
  260. }
  261. }
  262. }
  263. # TODO: detect indented paragraphs
  264. my $transfinal = $self->translate($paragraph,
  265. $self->{ref},
  266. "Plain text",
  267. "wrap" => $wrap);
  268. # TODO: preserve original line ends throughout the code instead
  269. chomp $transfinal;
  270. $transfinal .= "\n";
  271. $self->pushline( $transfinal );
  272. }
  273. 1;
  274. =head1 STATUS OF THIS MODULE
  275. Tested successfully on simple text files and NEWS.Debian files.
  276. =head1 AUTHORS
  277. Nicolas François <nicolas.francois@centraliens.net>
  278. =head1 COPYRIGHT AND LICENSE
  279. Copyright 2005-2008 by Nicolas FRANÇOIS <nicolas.francois@centraliens.net>.
  280. This program is free software; you may redistribute it and/or modify it
  281. under the terms of GPL (see the COPYING file).