po plugin: refresh POT files when a master page is updated
[ikiwiki.git] / IkiWiki / Plugin / po.pm
1 #!/usr/bin/perl
2 # .po as a wiki page type
3 # inspired by the GPL'd po4a-translate,
4 # which is Copyright 2002, 2003, 2004 by Martin Quinson (mquinson#debian.org)
5 package IkiWiki::Plugin::po;
6
7 use warnings;
8 use strict;
9 use IkiWiki 2.00;
10 use Encode;
11 use Locale::Po4a::Chooser;
12 use File::Basename;
13 use File::Spec;
14 use File::Temp;
15 use Memoize;
16
17 my %translations;
18 memoize("istranslatable");
19 memoize("_istranslation");
20
21 sub import {
22         hook(type => "getsetup", id => "po", call => \&getsetup);
23         hook(type => "checkconfig", id => "po", call => \&checkconfig);
24         hook(type => "needsbuild", id => "po", call => \&needsbuild);
25         hook(type => "targetpage", id => "po", call => \&targetpage);
26         hook(type => "tweakurlpath", id => "po", call => \&tweakurlpath);
27         hook(type => "tweakbestlink", id => "po", call => \&tweakbestlink);
28         hook(type => "filter", id => "po", call => \&filter);
29         hook(type => "htmlize", id => "po", call => \&htmlize);
30         hook(type => "pagetemplate", id => "po", call => \&pagetemplate);
31 }
32
33 sub getsetup () { #{{{
34         return
35                 plugin => {
36                         safe => 0,
37                         rebuild => 1, # format plugin
38                 },
39                 po_master_language => {
40                         type => "string",
41                         example => {
42                                 'code' => 'en',
43                                 'name' => 'English'
44                         },
45                         description => "master language (non-PO files)",
46                         safe => 1,
47                         rebuild => 1,
48                 },
49                 po_slave_languages => {
50                         type => "string",
51                         example => {
52                                 'fr' => 'Fran├žais',
53                                 'es' => 'Castellano',
54                                 'de' => 'Deutsch'
55                         },
56                         description => "slave languages (PO files)",
57                         safe => 1,
58                         rebuild => 1,
59                 },
60                 po_translatable_pages => {
61                         type => "pagespec",
62                         example => "!*/Discussion",
63                         description => "PageSpec controlling which pages are translatable",
64                         link => "ikiwiki/PageSpec",
65                         safe => 1,
66                         rebuild => 1,
67                 },
68                 po_link_to => {
69                         type => "string",
70                         example => "current",
71                         description => "internal linking behavior (default/current/negotiated)",
72                         safe => 1,
73                         rebuild => 1,
74                 },
75 } #}}}
76
77 sub checkconfig () { #{{{
78         foreach my $field (qw{po_master_language po_slave_languages}) {
79                 if (! exists $config{$field} || ! defined $config{$field}) {
80                         error(sprintf(gettext("Must specify %s"), $field));
81                 }
82         }
83         if (! exists $config{po_link_to} ||
84             ! defined $config{po_link_to}) {
85             $config{po_link_to}="default";
86         }
87         if (! exists $config{po_translatable_pages} ||
88             ! defined $config{po_translatable_pages}) {
89             $config{po_translatable_pages}="";
90         }
91         if ($config{po_link_to} eq "negotiated" && ! $config{usedirs}) {
92                 error(gettext("po_link_to=negotiated requires usedirs to be set"));
93         }
94         push @{$config{wiki_file_prune_regexps}}, qr/\.pot$/;
95 } #}}}
96
97 sub refreshpot ($) { #{{{
98         my $masterfile=shift;
99         (my $name, my $dir, my $suffix) = fileparse($masterfile, qr/\.[^.]*/);
100         my $potfile=File::Spec->catfile($dir, $name . ".pot");
101         my %options = ("markdown" => (pagetype($masterfile) eq 'mdwn') ? 1 : 0);
102         my $doc=Locale::Po4a::Chooser::new('text',%options);
103         $doc->read($masterfile);
104         $doc->{TT}{utf_mode} = 1;
105         $doc->{TT}{file_in_charset} = 'utf-8';
106         $doc->{TT}{file_out_charset} = 'utf-8';
107         $doc->parse or error("[po/refreshpot:$masterfile]: failed to parse");
108         $doc->writepo($potfile);
109 } #}}}
110
111 sub needsbuild () { #{{{
112         my $needsbuild=shift;
113
114         # build %translations, using istranslation's side-effect
115         foreach my $page (keys %pagesources) {
116                 istranslation($page);
117         }
118
119         foreach my $file (@$needsbuild) {
120                 refreshpot(srcfile($file)) if (istranslatable(pagename($file)));
121         }
122 } #}}}
123
124 sub targetpage (@) { #{{{
125         my %params = @_;
126         my $page=$params{page};
127         my $ext=$params{ext};
128
129         if (istranslation($page)) {
130                 my ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
131                 if (! $config{usedirs} || $page eq 'index') {
132                         return $masterpage . "." . $lang . "." . $ext;
133                 }
134                 else {
135                         return $masterpage . "/index." . $lang . "." . $ext;
136                 }
137         }
138         elsif (istranslatable($page)) {
139                 if (! $config{usedirs} || $page eq 'index') {
140                         return $page . "." . $config{po_master_language}{code} . "." . $ext;
141                 }
142                 else {
143                         return $page . "/index." . $config{po_master_language}{code} . "." . $ext;
144                 }
145         }
146         return;
147 } #}}}
148
149 sub tweakurlpath ($) { #{{{
150         my %params = @_;
151         my $url=$params{url};
152         if ($config{po_link_to} eq "negotiated") {
153                 $url =~ s!/index.$config{po_master_language}{code}.$config{htmlext}$!/!;
154         }
155         return $url;
156 } #}}}
157
158 sub tweakbestlink ($$) { #{{{
159         my %params = @_;
160         my $page=$params{page};
161         my $link=$params{link};
162         if ($config{po_link_to} eq "current"
163             && istranslatable($link)
164             && istranslation($page)) {
165                 my ($masterpage, $curlang) = ($page =~ /(.*)[.]([a-z]{2})$/);
166                 return $link . "." . $curlang;
167         }
168         return $link;
169 } #}}}
170
171 our %filtered;
172 # We use filter to convert PO to the master page's type,
173 # since other plugins should not work on PO files
174 sub filter (@) { #{{{
175         my %params = @_;
176         my $page = $params{page};
177         my $destpage = $params{destpage};
178         my $content = decode_utf8(encode_utf8($params{content}));
179
180         # decide if this is a PO file that should be converted into a translated document,
181         # and perform various sanity checks
182         if (! istranslation($page) || $filtered{$page}{$destpage}) {
183                 return $content;
184         }
185
186         my ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
187         my $file=srcfile(exists $params{file} ? $params{file} : $IkiWiki::pagesources{$page});
188         my $masterfile = srcfile($pagesources{$masterpage});
189         my (@pos,@masters);
190         push @pos,$file;
191         push @masters,$masterfile;
192         my %options = (
193                         "markdown" => (pagetype($masterfile) eq 'mdwn') ? 1 : 0,
194                         );
195         my $doc=Locale::Po4a::Chooser::new('text',%options);
196         $doc->process(
197                 'po_in_name'    => \@pos,
198                 'file_in_name'  => \@masters,
199                 'file_in_charset'  => 'utf-8',
200                 'file_out_charset' => 'utf-8',
201         ) or error("[po/filter:$file]: failed to translate");
202         my ($percent,$hit,$queries) = $doc->stats();
203         my $tmpfh = File::Temp->new(TEMPLATE => "/tmp/ikiwiki-po-filter-out.XXXXXXXXXX");
204         my $tmpout = $tmpfh->filename;
205         $doc->write($tmpout) or error("[po/filter:$file] could not write $tmpout");
206         $content = readfile($tmpout) or error("[po/filter:$file] could not read $tmpout");
207         $filtered{$page}{$destpage}=1;
208         return $content;
209 } #}}}
210
211 sub htmlize (@) { #{{{
212         my %params=@_;
213         my $page = $params{page};
214         my $content = $params{content};
215         my ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
216         my $masterfile = srcfile($pagesources{$masterpage});
217
218         # force content to be htmlize'd as if it was the same type as the master page
219         return IkiWiki::htmlize($page, $page, pagetype($masterfile), $content);
220 } #}}}
221
222 sub otherlanguages ($) { #{{{
223         my $page=shift;
224         my @ret;
225         if (istranslatable($page)) {
226                 foreach my $lang (sort keys %{$translations{$page}}) {
227                         push @ret, {
228                                 url => urlto($translations{$page}{$lang}, $page),
229                                 code => $lang,
230                                 language => $config{po_slave_languages}{$lang},
231                                 master => 0,
232                         };
233                 }
234         }
235         elsif (istranslation($page)) {
236                 my ($masterpage, $curlang) = ($page =~ /(.*)[.]([a-z]{2})$/);
237                 push @ret, {
238                         url => urlto($masterpage, $page),
239                         code => $config{po_master_language}{code},
240                         language => $config{po_master_language}{name},
241                         master => 1,
242                 };
243                 foreach my $lang (sort keys %{$translations{$masterpage}}) {
244                         push @ret, {
245                                 url => urlto($translations{$masterpage}{$lang}, $page),
246                                 code => $lang,
247                                 language => $config{po_slave_languages}{$lang},
248                                 master => 0,
249                         } unless ($lang eq $curlang);
250                 }
251         }
252         return @ret;
253 } #}}}
254
255 sub pagetemplate (@) { #{{{
256         my %params=@_;
257         my $page=$params{page};
258         my $template=$params{template};
259
260         if ($template->query(name => "otherlanguages")) {
261                 $template->param(otherlanguages => [otherlanguages($page)]);
262         }
263 } # }}}
264
265 sub istranslatable ($) { #{{{
266         my $page=shift;
267         my $file=$pagesources{$page};
268
269         if (! defined $file
270             || (defined pagetype($file) && pagetype($file) eq 'po')
271             || $file =~ /\.pot$/) {
272                 return 0;
273         }
274         return pagespec_match($page, $config{po_translatable_pages});
275 } #}}}
276
277 sub _istranslation ($) { #{{{
278         my $page=shift;
279         my $file=$pagesources{$page};
280         if (! defined $file) {
281                 return IkiWiki::FailReason->new("no file specified");
282         }
283
284         if (! defined $file
285             || ! defined pagetype($file)
286             || ! pagetype($file) eq 'po'
287             || $file =~ /\.pot$/) {
288                 return 0;
289         }
290
291         my ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
292         if (! defined $masterpage || ! defined $lang
293             || ! (length($masterpage) > 0) || ! (length($lang) > 0)
294             || ! defined $pagesources{$masterpage}
295             || ! defined $config{po_slave_languages}{$lang}) {
296                 return 0;
297         }
298
299         return istranslatable($masterpage);
300 } #}}}
301
302 sub istranslation ($) { #{{{
303         my $page=shift;
304         if (_istranslation($page)) {
305                 my ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
306                 $translations{$masterpage}{$lang}=$page unless exists $translations{$masterpage}{$lang};
307                 return 1;
308         }
309         return 0;
310 } #}}}
311
312 package IkiWiki::PageSpec;
313 use warnings;
314 use strict;
315 use IkiWiki 2.00;
316
317 sub match_istranslation ($;@) { #{{{
318         my $page=shift;
319         if (IkiWiki::Plugin::po::istranslation($page)) {
320                 return IkiWiki::SuccessReason->new("is a translation page");
321         }
322         else {
323                 return IkiWiki::FailReason->new("is not a translation page");
324         }
325 } #}}}
326
327 sub match_istranslatable ($;@) { #{{{
328         my $page=shift;
329         if (IkiWiki::Plugin::po::istranslatable($page)) {
330                 return IkiWiki::SuccessReason->new("is set as translatable in po_translatable_pages");
331         }
332         else {
333                 return IkiWiki::FailReason->new("is not set as translatable in po_translatable_pages");
334         }
335 } #}}}
336
337 1