po plugin: build %translations at scan time
[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::Temp;
13 use Memoize;
14
15 my %translations;
16 memoize("istranslatable");
17 memoize("_istranslation");
18
19 sub import {
20         hook(type => "getsetup", id => "po", call => \&getsetup);
21         hook(type => "checkconfig", id => "po", call => \&checkconfig);
22         hook(type => "scan", id => "po", call => \&scan);
23         hook(type => "targetpage", id => "po", call => \&targetpage);
24         hook(type => "tweakurlpath", id => "po", call => \&tweakurlpath);
25         hook(type => "tweakbestlink", id => "po", call => \&tweakbestlink);
26         hook(type => "filter", id => "po", call => \&filter);
27         hook(type => "htmlize", id => "po", call => \&htmlize);
28 }
29
30 sub getsetup () { #{{{
31         return
32                 plugin => {
33                         safe => 0,
34                         rebuild => 1, # format plugin
35                 },
36                 po_master_language => {
37                         type => "string",
38                         example => {
39                                 'code' => 'en',
40                                 'name' => 'English'
41                         },
42                         description => "master language (non-PO files)",
43                         safe => 1,
44                         rebuild => 1,
45                 },
46                 po_slave_languages => {
47                         type => "string",
48                         example => {'fr' => { 'name' => 'Fran├žais' },
49                                     'es' => { 'name' => 'Castellano' },
50                                     'de' => { 'name' => 'Deutsch' },
51                         },
52                         description => "slave languages (PO files)",
53                         safe => 1,
54                         rebuild => 1,
55                 },
56                 po_translatable_pages => {
57                         type => "pagespec",
58                         example => "!*/Discussion",
59                         description => "PageSpec controlling which pages are translatable",
60                         link => "ikiwiki/PageSpec",
61                         safe => 1,
62                         rebuild => 1,
63                 },
64                 po_link_to => {
65                         type => "string",
66                         example => "current",
67                         description => "internal linking behavior (default/current/negotiated)",
68                         safe => 1,
69                         rebuild => 1,
70                 },
71 } #}}}
72
73 sub checkconfig () { #{{{
74         foreach my $field (qw{po_master_language po_slave_languages}) {
75                 if (! exists $config{$field} || ! defined $config{$field}) {
76                         error(sprintf(gettext("Must specify %s"), $field));
77                 }
78         }
79         if (! exists $config{po_link_to} ||
80             ! defined $config{po_link_to}) {
81             $config{po_link_to}="default";
82         }
83         if (! exists $config{po_translatable_pages} ||
84             ! defined $config{po_translatable_pages}) {
85             $config{po_translatable_pages}="";
86         }
87         if ($config{po_link_to} eq "negotiated" && ! $config{usedirs}) {
88                 error(gettext("po_link_to=negotiated requires usedirs to be set"));
89         }
90         push @{$config{wiki_file_prune_regexps}}, qr/\.pot$/;
91 } #}}}
92
93 sub scan (@) { #{{{
94         my %params=@_;
95         my $page=$params{page};
96         # let's build %translations, using istranslation's
97         # side-effect, so that we can consider it is complete at
98         # preprocess time
99         istranslation($page);
100 } #}}}
101
102 sub targetpage (@) { #{{{
103         my %params = @_;
104         my $page=$params{page};
105         my $ext=$params{ext};
106
107         if (istranslation($page)) {
108                 my ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
109                 if (! $config{usedirs} || $page eq 'index') {
110                         return $masterpage . "." . $lang . "." . $ext;
111                 }
112                 else {
113                         return $masterpage . "/index." . $lang . "." . $ext;
114                 }
115         }
116         elsif (istranslatable($page)) {
117                 if (! $config{usedirs} || $page eq 'index') {
118                         return $page . "." . $config{po_master_language}{code} . "." . $ext;
119                 }
120                 else {
121                         return $page . "/index." . $config{po_master_language}{code} . "." . $ext;
122                 }
123         }
124         return;
125 } #}}}
126
127 sub tweakurlpath ($) { #{{{
128         my %params = @_;
129         my $url=$params{url};
130         if ($config{po_link_to} eq "negotiated") {
131                 $url =~ s!/index.$config{po_master_language}{code}.$config{htmlext}$!/!;
132         }
133         return $url;
134 } #}}}
135
136 sub tweakbestlink ($$) { #{{{
137         my %params = @_;
138         my $page=$params{page};
139         my $link=$params{link};
140         if ($config{po_link_to} eq "current"
141             && istranslatable($link)
142             && istranslation($page)) {
143                 my ($masterpage, $curlang) = ($page =~ /(.*)[.]([a-z]{2})$/);
144                 return $link . "." . $curlang;
145         }
146         return $link;
147 } #}}}
148
149 our %filtered;
150 # We use filter to convert PO to the master page's type,
151 # since other plugins should not work on PO files
152 sub filter (@) { #{{{
153         my %params = @_;
154         my $page = $params{page};
155         my $destpage = $params{destpage};
156         my $content = decode_utf8(encode_utf8($params{content}));
157
158         # decide if this is a PO file that should be converted into a translated document,
159         # and perform various sanity checks
160         if (! istranslation($page) || $filtered{$page}{$destpage}) {
161                 return $content;
162         }
163
164         my ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
165         my $file=srcfile(exists $params{file} ? $params{file} : $IkiWiki::pagesources{$page});
166         my $masterfile = srcfile($pagesources{$masterpage});
167         my (@pos,@masters);
168         push @pos,$file;
169         push @masters,$masterfile;
170         my %options = (
171                         "markdown" => (pagetype($masterfile) eq 'mdwn') ? 1 : 0,
172                         );
173         my $doc=Locale::Po4a::Chooser::new('text',%options);
174         $doc->process(
175                 'po_in_name'    => \@pos,
176                 'file_in_name'  => \@masters,
177                 'file_in_charset'  => 'utf-8',
178                 'file_out_charset' => 'utf-8',
179         ) or error("[po/filter:$file]: failed to translate");
180         my ($percent,$hit,$queries) = $doc->stats();
181         my $tmpfh = File::Temp->new(TEMPLATE => "/tmp/ikiwiki-po-filter-out.XXXXXXXXXX");
182         my $tmpout = $tmpfh->filename;
183         $doc->write($tmpout) or error("[po/filter:$file] could not write $tmpout");
184         $content = readfile($tmpout) or error("[po/filter:$file] could not read $tmpout");
185         $filtered{$page}{$destpage}=1;
186         return $content;
187 } #}}}
188
189 sub htmlize (@) { #{{{
190         my %params=@_;
191         my $page = $params{page};
192         my $content = $params{content};
193         my ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
194         my $masterfile = srcfile($pagesources{$masterpage});
195
196         # force content to be htmlize'd as if it was the same type as the master page
197         return IkiWiki::htmlize($page, $page, pagetype($masterfile), $content);
198 } #}}}
199
200 sub istranslatable ($) { #{{{
201         my $page=shift;
202         my $file=$pagesources{$page};
203
204         if (! defined $file
205             || (defined pagetype($file) && pagetype($file) eq 'po')
206             || $file =~ /\.pot$/) {
207                 return 0;
208         }
209         return pagespec_match($page, $config{po_translatable_pages});
210 } #}}}
211
212 sub _istranslation ($) { #{{{
213         my $page=shift;
214         my $file=$pagesources{$page};
215         if (! defined $file) {
216                 return IkiWiki::FailReason->new("no file specified");
217         }
218
219         if (! defined $file
220             || ! defined pagetype($file)
221             || ! pagetype($file) eq 'po'
222             || $file =~ /\.pot$/) {
223                 return 0;
224         }
225
226         my ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
227         if (! defined $masterpage || ! defined $lang
228             || ! (length($masterpage) > 0) || ! (length($lang) > 0)
229             || ! defined $pagesources{$masterpage}
230             || ! defined $config{po_slave_languages}{$lang}) {
231                 return 0;
232         }
233
234         return istranslatable($masterpage);
235 } #}}}
236
237 sub istranslation ($) { #{{{
238         my $page=shift;
239         if (_istranslation($page)) {
240                 my ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
241                 $translations{$masterpage}{$lang}=$page unless exists $translations{$masterpage}{$lang};
242                 return 1;
243         }
244         return 0;
245 } #}}}
246
247
248 package IkiWiki::PageSpec;
249 use warnings;
250 use strict;
251 use IkiWiki 2.00;
252
253 sub match_istranslation ($;@) { #{{{
254         my $page=shift;
255         if (IkiWiki::Plugin::po::istranslation($page)) {
256                 return IkiWiki::SuccessReason->new("is a translation page");
257         }
258         else {
259                 return IkiWiki::FailReason->new("is not a translation page");
260         }
261 } #}}}
262
263 sub match_istranslatable ($;@) { #{{{
264         my $page=shift;
265         if (IkiWiki::Plugin::po::istranslatable($page)) {
266                 return IkiWiki::SuccessReason->new("is set as translatable in po_translatable_pages");
267         }
268         else {
269                 return IkiWiki::FailReason->new("is not set as translatable in po_translatable_pages");
270         }
271 } #}}}
272
273 1