]>
Commit | Line | Data |
---|---|---|
589b789c TC |
1 | package BSE::Message; |
2 | use strict; | |
3 | use Carp qw/confess/; | |
ebc63b18 TC |
4 | use BSE::DB; |
5 | use BSE::Cfg; | |
9aecfc15 | 6 | use BSE::Cache; |
ebec7b06 | 7 | use DevHelp::HTML; |
c0bf9781 | 8 | use Scalar::Util qw(reftype blessed); |
07edb12d TC |
9 | use overload |
10 | "&{}" => sub { my $self = $_[0]; return sub { $self->_old_msg(@_) } }, | |
11 | "bool" => sub { 1 }; | |
ebc63b18 | 12 | |
e0ed81d7 | 13 | our $VERSION = "1.008"; |
cb7fd78d | 14 | |
9aecfc15 TC |
15 | my $single; |
16 | ||
ebc63b18 TC |
17 | =head1 NAME |
18 | ||
19 | BSE::Message - BSE message catalog access. | |
20 | ||
21 | =head1 SYNOPSIS | |
22 | ||
23 | my $msgs = BSE::Message->new; | |
24 | my $text = $msgs->text($lang, $msgid); | |
25 | my $text = $msgs->text($lang, $msgid, [ parameters ]); | |
26 | my $text = $msgs->text($lang, $msgid, [ parameters ], $def); | |
27 | my $html = $msgs->html($lang, $msgid); | |
28 | my $html = $msgs->html($lang, $msgid, [ parameters ]); | |
29 | my $html = $msgs->html($lang, $msgid, [ parameters ], $def); | |
30 | ||
31 | =cut | |
589b789c | 32 | |
9aecfc15 TC |
33 | sub _new { |
34 | my ($class, %opts) = @_; | |
35 | ||
36 | return bless | |
37 | { | |
38 | cache_age => BSE::Cfg->single->entry("messages", "cache_age", 60), | |
39 | mycache => {}, | |
40 | cache => scalar(BSE::Cache->load), | |
41 | }, $class; | |
42 | } | |
43 | ||
589b789c TC |
44 | sub new { |
45 | my ($class, %opts) = @_; | |
46 | ||
9aecfc15 TC |
47 | $single ||= $class->_new; |
48 | ||
49 | if ($opts{section}) { | |
50 | $single->{section} = $opts{section}; | |
51 | } | |
ebc63b18 | 52 | |
9aecfc15 | 53 | return $single; |
ebc63b18 TC |
54 | } |
55 | ||
56 | sub text { | |
57 | my ($self, $lang, $msgid, $parms, $def) = @_; | |
58 | ||
d8913cb9 TC |
59 | $msgid =~ s/^msg://; |
60 | ||
9aecfc15 TC |
61 | ref $self or $self = $self->new; |
62 | ||
ebc63b18 TC |
63 | my $msg = $self->_get_replaced($lang, $msgid, $parms); |
64 | if ($msg) { | |
65 | if ($msg->{formatting} eq 'body') { | |
66 | require BSE::Formatter; | |
67 | my $formatter = BSE::Formatter->new; | |
68 | return $formatter->remove_format($msg->{message}); | |
69 | } | |
70 | ||
71 | return $msg->{message}; | |
72 | } | |
73 | else { | |
74 | $def and return $def; | |
75 | return; | |
76 | } | |
77 | } | |
78 | ||
79 | sub html { | |
80 | my ($self, $lang, $msgid, $parms, $def) = @_; | |
81 | ||
d8913cb9 TC |
82 | $msgid =~ s/^msg://; |
83 | ||
9aecfc15 TC |
84 | ref $self or $self = $self->new; |
85 | ||
ebc63b18 TC |
86 | my $msg = $self->_get_replaced($lang, $msgid, $parms); |
87 | if ($msg) { | |
88 | if ($msg->{formatting} eq 'body') { | |
9785c9c4 | 89 | require BSE::Generate; |
ebc63b18 TC |
90 | require BSE::Template; |
91 | my $gen = Generate->new(cfg => BSE::Cfg->single); | |
92 | my $templater = BSE::Template->templater(BSE::Cfg->single); | |
93 | return $gen->format_body(acts => {}, | |
e0ed81d7 | 94 | articles => "BSE::TB::Articles", |
ebc63b18 TC |
95 | text => $msg->{message}, |
96 | templater => $templater); | |
97 | } | |
98 | ||
99 | return escape_html($msg->{message}); | |
100 | } | |
101 | else { | |
102 | $def and return $def; | |
103 | return; | |
104 | } | |
105 | } | |
106 | ||
107 | =item $self->get_replaced($lang, $msgid, $parms) | |
108 | ||
109 | Retrieve the base message text + formatting info, and replace parameters. | |
110 | ||
111 | Currently just replaces parameters, should also: | |
112 | ||
113 | =over | |
114 | ||
115 | =item * | |
116 | ||
117 | have a mechanism to replace messages eg. | |
118 | ||
119 | %msg{bse/bar} | |
120 | ||
121 | =item * | |
122 | ||
123 | have a zero/single/plural mechanism, eg.: | |
124 | ||
125 | %c1{zero text;single text;multiple text} | |
126 | ||
127 | =back | |
128 | ||
129 | =cut | |
130 | ||
131 | sub _get_replaced { | |
132 | my ($self, $lang, $msgid, $parms) = @_; | |
133 | ||
134 | my $msg = $self->_get_base($lang, $msgid) | |
135 | or return; | |
136 | ||
137 | $parms ||= []; | |
495114d1 | 138 | $msg->{message} =~ s/%(%|[0-9]+:(?:\{\w+\})?[-+ #0]?[0-9]*(?:\.[0-9]+)?[duoxXfFeEgGs])/ |
ebc63b18 TC |
139 | $1 eq "%" ? "%" : $self->_value($msg, $1, $parms)/ge; |
140 | ||
141 | return $msg; | |
142 | } | |
143 | ||
144 | sub _value { | |
145 | my ($self, $msg, $code, $parms) = @_; | |
146 | ||
147 | my ($index, $format) = $code =~ /^([0-9]+):(.*)$/; | |
148 | $index >= 1 && $index <= @$parms | |
149 | or return "(bad index $index in %$code)"; | |
150 | ||
495114d1 TC |
151 | my $method = "describe"; |
152 | if ($format =~ s/^\{(\w+)\}//) { | |
153 | my $work = $1; | |
154 | unless ($work =~ /^(remove|save|new)$/) { | |
155 | $method = $work; | |
156 | } | |
157 | } | |
158 | ||
ebc63b18 | 159 | my $value = $parms->[$index-1]; |
495114d1 | 160 | if (ref $value) { |
ebc63b18 | 161 | local $@; |
c0bf9781 TC |
162 | if (blessed $value) { |
163 | my $good = eval { $value = $value->$method; 1; }; | |
164 | unless ($good) { | |
165 | return "(Bad parameter $index - blessed but no $method)"; | |
166 | } | |
167 | } | |
168 | elsif (reftype $value eq "HASH") { | |
169 | defined $value->{$method} | |
170 | or return "(Unknown key $method for $index)"; | |
171 | $value = $value->{$method}; | |
172 | } | |
173 | else { | |
174 | return "(Can't handle ".reftype($value)." values)"; | |
ebc63b18 TC |
175 | } |
176 | } | |
177 | ||
178 | return sprintf "%$format", $value; | |
179 | } | |
180 | ||
181 | =item $self->get_base($lang, $msgid) | |
182 | ||
183 | Retrieve the base message text + formatting info. | |
184 | ||
185 | =cut | |
186 | ||
187 | sub _get_base { | |
188 | my ($self, $lang, $msgid) = @_; | |
189 | ||
190 | defined $lang or $lang = BSE::Cfg->single->entry("basic", "language_code", "en"); | |
191 | ||
192 | if ($self->{cache_age} == 0) { | |
193 | return $self->_get_base_low($lang, $msgid); | |
194 | } | |
195 | ||
196 | my $key = "$lang.$msgid"; | |
197 | my $entry = $self->{mycache}{$key}; | |
198 | if (!$entry && $self->{cache}) { | |
199 | $entry = $self->{cache}->get("msg-$key"); | |
200 | } | |
201 | ||
202 | my $now = time; | |
203 | if ($entry) { | |
204 | if ($entry->[0] < $now - $self->{cache_age}) { | |
205 | undef $entry; | |
206 | } | |
207 | } | |
208 | ||
07edb12d TC |
209 | if ($entry) { |
210 | # clone the entry so text replacement doesn't mess us up | |
63e7203f | 211 | $entry->[1] or return; |
055e6576 | 212 | my %entry = %{$entry->[1]}; |
07edb12d TC |
213 | return \%entry; |
214 | } | |
ebc63b18 TC |
215 | |
216 | my $msg = $self->_get_base_low($lang, $msgid); | |
217 | $entry = [ $now, $msg ]; | |
218 | $self->{mycache}{$key} = $entry; | |
219 | if ($self->{cache}) { | |
220 | $self->{cache}->set("msg-$key", $entry); | |
221 | } | |
222 | ||
3f9c8a96 TC |
223 | $msg or return; |
224 | ||
055e6576 TC |
225 | # clone so the caller doesn't modify cached value |
226 | my %entry = %$msg; | |
227 | return \%entry; | |
ebc63b18 TC |
228 | } |
229 | ||
230 | sub _get_base_low { | |
231 | my ($self, $lang, $msgid) = @_; | |
232 | ||
233 | # build a list of languages to search | |
234 | my @langs = $lang; | |
defb9881 TC |
235 | if ($lang =~ /^([a-z]+(?:[a-z]+))\./) { |
236 | push @langs, $1; | |
237 | } | |
238 | if ($lang =~ /^([a-z]+)_/i) { | |
ebc63b18 TC |
239 | push @langs, $1; |
240 | } | |
241 | ||
242 | my $msg; | |
243 | for my $search_lang (@langs) { | |
244 | ($msg) = BSE::DB->query(bseGetMsgManaged => $msgid, $search_lang) | |
245 | and return $msg; | |
246 | } | |
247 | ||
248 | for my $search_lang (@langs) { | |
249 | ($msg) = BSE::DB->query(bseGetMsgDefault => $msgid, $search_lang) | |
250 | and return $msg; | |
251 | } | |
252 | ||
253 | for my $fallback ($self->fallback) { | |
254 | ($msg) = BSE::DB->query(bseGetMsgManaged => $msgid, $fallback) | |
255 | and return $msg; | |
256 | ||
257 | ($msg) = BSE::DB->query(bseGetMsgDefault => $msgid, "") | |
258 | and return $msg; | |
259 | } | |
260 | ||
261 | return; | |
262 | } | |
263 | ||
264 | sub _old_msg { | |
265 | my ($self, $msgid, $def, @parms) = @_; | |
266 | ||
267 | my $msg = BSE::Cfg->single->entry("messages", "$self->{section}/$msgid"); | |
268 | if ($msg) { | |
269 | $msg =~ s/\$([\d\$])/$1 eq '$' ? '$' : $parms[$1-1]/eg; | |
270 | return $msg; | |
271 | } | |
272 | ||
ebc63b18 TC |
273 | $msgid = "bse/$self->{section}/$msgid"; |
274 | my $text = $self->text(undef, $msgid, \@parms); | |
275 | $text and return $text; | |
276 | ||
277 | return $def; | |
278 | } | |
279 | ||
280 | sub languages { | |
281 | my ($self) = @_; | |
282 | ||
283 | my $cfg = BSE::Cfg->single; | |
284 | my %langs = $cfg->entries("languages"); | |
285 | delete $langs{fallback}; | |
286 | $langs{en} ||= "English"; | |
287 | my @langs = map +{ id => $_, name => $langs{$_} }, sort keys %langs; | |
288 | ||
289 | return @langs; | |
290 | } | |
291 | ||
292 | sub fallback { | |
293 | my ($self) = @_; | |
294 | ||
295 | my $cfg = BSE::Cfg->single; | |
296 | my $fallback = $cfg->entry("languages", "fallback", "en"); | |
297 | return split /,/, $fallback; | |
298 | } | |
299 | ||
300 | sub uncache { | |
9aecfc15 | 301 | my ($self, $id) = @_; |
ebc63b18 | 302 | |
9aecfc15 TC |
303 | ref $self or $self = $self->new; |
304 | ||
305 | my $cache = $self->{cache} | |
306 | or return; | |
ebc63b18 TC |
307 | |
308 | for my $lang ($self->languages) { | |
309 | $cache->delete("msg-$lang->{id}.$id"); | |
310 | } | |
589b789c TC |
311 | } |
312 | ||
313 | 1; |