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