Coverage Report

Created: 2025-12-31 06:35

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/wxwidgets/src/common/translation.cpp
Line
Count
Source
1
/////////////////////////////////////////////////////////////////////////////
2
// Name:        src/common/translation.cpp
3
// Purpose:     Internationalization and localisation for wxWidgets
4
// Author:      Vadim Zeitlin, Vaclav Slavik,
5
//              Michael N. Filippov <michael@idisys.iae.nsk.su>
6
//              (2003/09/30 - PluralForms support)
7
// Created:     2010-04-23
8
// Copyright:   (c) 1998 Vadim Zeitlin <zeitlin@dptmaths.ens-cachan.fr>
9
// Licence:     wxWindows licence
10
/////////////////////////////////////////////////////////////////////////////
11
12
// ============================================================================
13
// declaration
14
// ============================================================================
15
16
// ----------------------------------------------------------------------------
17
// headers
18
// ----------------------------------------------------------------------------
19
20
// For compilers that support precompilation, includes "wx.h".
21
#include "wx/wxprec.h"
22
23
24
#if wxUSE_INTL
25
26
#ifndef WX_PRECOMP
27
    #include "wx/dynarray.h"
28
    #include "wx/string.h"
29
    #include "wx/intl.h"
30
    #include "wx/log.h"
31
    #include "wx/utils.h"
32
    #include "wx/module.h"
33
#endif // WX_PRECOMP
34
35
// standard headers
36
#include <ctype.h>
37
#include <stdlib.h>
38
39
#include "wx/arrstr.h"
40
#include "wx/dir.h"
41
#include "wx/file.h"
42
#include "wx/filename.h"
43
#include "wx/tokenzr.h"
44
#include "wx/fontmap.h"
45
#include "wx/stdpaths.h"
46
#include "wx/version.h"
47
#include "wx/uilocale.h"
48
49
#ifdef __WINDOWS__
50
    #include "wx/dynlib.h"
51
    #include "wx/scopedarray.h"
52
    #include "wx/msw/wrapwin.h"
53
    #include "wx/msw/missing.h"
54
#endif
55
56
#ifdef __MINGW32__
57
    #include <map>
58
#endif
59
60
#include <memory>
61
#include <unordered_set>
62
63
// ----------------------------------------------------------------------------
64
// simple types
65
// ----------------------------------------------------------------------------
66
67
typedef wxUint32 size_t32;
68
69
// ----------------------------------------------------------------------------
70
// constants
71
// ----------------------------------------------------------------------------
72
73
// magic number identifying the .mo format file
74
const size_t32 MSGCATALOG_MAGIC    = 0x950412de;
75
const size_t32 MSGCATALOG_MAGIC_SW = 0xde120495;
76
77
0
#define TRACE_I18N wxS("i18n")
78
79
// ============================================================================
80
// implementation
81
// ============================================================================
82
83
namespace
84
{
85
86
// ----------------------------------------------------------------------------
87
// Platform specific helpers
88
// ----------------------------------------------------------------------------
89
90
#if wxUSE_LOG_TRACE
91
92
void LogTraceArray(const char *prefix, const wxVector<wxString>& arr)
93
0
{
94
0
    wxString s;
95
0
    for (wxVector<wxString>::const_iterator j = arr.begin(); j != arr.end(); ++j)
96
0
    {
97
0
        if (j != arr.begin())
98
0
            s += ",";
99
0
        s += *j;
100
0
    }
101
0
    wxLogTrace(TRACE_I18N, "%s: [%s]", prefix, s);
102
0
}
103
104
void LogTraceLargeArray(const wxString& prefix, const wxArrayString& arr)
105
0
{
106
0
    wxLogTrace(TRACE_I18N, "%s:", prefix);
107
0
    for ( wxArrayString::const_iterator i = arr.begin(); i != arr.end(); ++i )
108
0
        wxLogTrace(TRACE_I18N, "    %s", *i);
109
0
}
110
111
#else // !wxUSE_LOG_TRACE
112
113
#define LogTraceArray(prefix, arr)
114
#define LogTraceLargeArray(prefix, arr)
115
116
#endif // wxUSE_LOG_TRACE/!wxUSE_LOG_TRACE
117
118
wxString GetPreferredUILanguage(const wxVector<wxString>& available)
119
0
{
120
0
    const wxVector<wxString>& preferred = wxUILocale::GetPreferredUILanguages();
121
0
    LogTraceArray(" - system preferred languages", preferred);
122
123
0
    return wxLocaleIdent::GetBestMatch(preferred, available);
124
0
}
125
126
} // anonymous namespace
127
128
// ----------------------------------------------------------------------------
129
// Plural forms parser
130
// ----------------------------------------------------------------------------
131
132
/*
133
                                Simplified Grammar
134
135
Expression:
136
    LogicalOrExpression '?' Expression ':' Expression
137
    LogicalOrExpression
138
139
LogicalOrExpression:
140
    LogicalAndExpression "||" LogicalOrExpression   // to (a || b) || c
141
    LogicalAndExpression
142
143
LogicalAndExpression:
144
    EqualityExpression "&&" LogicalAndExpression    // to (a && b) && c
145
    EqualityExpression
146
147
EqualityExpression:
148
    RelationalExpression "==" RelationalExperession
149
    RelationalExpression "!=" RelationalExperession
150
    RelationalExpression
151
152
RelationalExpression:
153
    MultiplicativeExpression '>' MultiplicativeExpression
154
    MultiplicativeExpression '<' MultiplicativeExpression
155
    MultiplicativeExpression ">=" MultiplicativeExpression
156
    MultiplicativeExpression "<=" MultiplicativeExpression
157
    MultiplicativeExpression
158
159
MultiplicativeExpression:
160
    PmExpression '%' PmExpression
161
    PmExpression
162
163
PmExpression:
164
    N
165
    Number
166
    '(' Expression ')'
167
*/
168
169
class wxPluralFormsToken
170
{
171
public:
172
    enum Type
173
    {
174
        T_ERROR, T_EOF, T_NUMBER, T_N, T_PLURAL, T_NPLURALS, T_EQUAL, T_ASSIGN,
175
        T_GREATER, T_GREATER_OR_EQUAL, T_LESS, T_LESS_OR_EQUAL,
176
        T_REMINDER, T_NOT_EQUAL,
177
        T_LOGICAL_AND, T_LOGICAL_OR, T_QUESTION, T_COLON, T_SEMICOLON,
178
        T_LEFT_BRACKET, T_RIGHT_BRACKET
179
    };
180
0
    Type type() const { return m_type; }
181
0
    void setType(Type t) { m_type = t; }
182
    // for T_NUMBER only
183
    typedef int Number;
184
0
    Number number() const { return m_number; }
185
0
    void setNumber(Number num) { m_number = num; }
186
private:
187
    Type m_type;
188
    Number m_number;
189
};
190
191
192
class wxPluralFormsScanner
193
{
194
public:
195
    wxPluralFormsScanner(const char* s);
196
0
    const wxPluralFormsToken& token() const { return m_token; }
197
    bool nextToken();  // returns false if error
198
private:
199
    const char* m_s;
200
    wxPluralFormsToken m_token;
201
};
202
203
0
wxPluralFormsScanner::wxPluralFormsScanner(const char* s) : m_s(s)
204
0
{
205
0
    nextToken();
206
0
}
207
208
bool wxPluralFormsScanner::nextToken()
209
0
{
210
0
    wxPluralFormsToken::Type type = wxPluralFormsToken::T_ERROR;
211
0
    while (isspace((unsigned char) *m_s))
212
0
    {
213
0
        ++m_s;
214
0
    }
215
0
    if (*m_s == 0)
216
0
    {
217
0
        type = wxPluralFormsToken::T_EOF;
218
0
    }
219
0
    else if (isdigit((unsigned char) *m_s))
220
0
    {
221
0
        wxPluralFormsToken::Number number = *m_s++ - '0';
222
0
        while (isdigit((unsigned char) *m_s))
223
0
        {
224
0
            number = number * 10 + (*m_s++ - '0');
225
0
        }
226
0
        m_token.setNumber(number);
227
0
        type = wxPluralFormsToken::T_NUMBER;
228
0
    }
229
0
    else if (isalpha((unsigned char) *m_s))
230
0
    {
231
0
        const char* begin = m_s++;
232
0
        while (isalnum((unsigned char) *m_s))
233
0
        {
234
0
            ++m_s;
235
0
        }
236
0
        size_t size = m_s - begin;
237
0
        if (size == 1 && memcmp(begin, "n", size) == 0)
238
0
        {
239
0
            type = wxPluralFormsToken::T_N;
240
0
        }
241
0
        else if (size == 6 && memcmp(begin, "plural", size) == 0)
242
0
        {
243
0
            type = wxPluralFormsToken::T_PLURAL;
244
0
        }
245
0
        else if (size == 8 && memcmp(begin, "nplurals", size) == 0)
246
0
        {
247
0
            type = wxPluralFormsToken::T_NPLURALS;
248
0
        }
249
0
    }
250
0
    else if (*m_s == '=')
251
0
    {
252
0
        ++m_s;
253
0
        if (*m_s == '=')
254
0
        {
255
0
            ++m_s;
256
0
            type = wxPluralFormsToken::T_EQUAL;
257
0
        }
258
0
        else
259
0
        {
260
0
            type = wxPluralFormsToken::T_ASSIGN;
261
0
        }
262
0
    }
263
0
    else if (*m_s == '>')
264
0
    {
265
0
        ++m_s;
266
0
        if (*m_s == '=')
267
0
        {
268
0
            ++m_s;
269
0
            type = wxPluralFormsToken::T_GREATER_OR_EQUAL;
270
0
        }
271
0
        else
272
0
        {
273
0
            type = wxPluralFormsToken::T_GREATER;
274
0
        }
275
0
    }
276
0
    else if (*m_s == '<')
277
0
    {
278
0
        ++m_s;
279
0
        if (*m_s == '=')
280
0
        {
281
0
            ++m_s;
282
0
            type = wxPluralFormsToken::T_LESS_OR_EQUAL;
283
0
        }
284
0
        else
285
0
        {
286
0
            type = wxPluralFormsToken::T_LESS;
287
0
        }
288
0
    }
289
0
    else if (*m_s == '%')
290
0
    {
291
0
        ++m_s;
292
0
        type = wxPluralFormsToken::T_REMINDER;
293
0
    }
294
0
    else if (*m_s == '!' && m_s[1] == '=')
295
0
    {
296
0
        m_s += 2;
297
0
        type = wxPluralFormsToken::T_NOT_EQUAL;
298
0
    }
299
0
    else if (*m_s == '&' && m_s[1] == '&')
300
0
    {
301
0
        m_s += 2;
302
0
        type = wxPluralFormsToken::T_LOGICAL_AND;
303
0
    }
304
0
    else if (*m_s == '|' && m_s[1] == '|')
305
0
    {
306
0
        m_s += 2;
307
0
        type = wxPluralFormsToken::T_LOGICAL_OR;
308
0
    }
309
0
    else if (*m_s == '?')
310
0
    {
311
0
        ++m_s;
312
0
        type = wxPluralFormsToken::T_QUESTION;
313
0
    }
314
0
    else if (*m_s == ':')
315
0
    {
316
0
        ++m_s;
317
0
        type = wxPluralFormsToken::T_COLON;
318
0
    } else if (*m_s == ';') {
319
0
        ++m_s;
320
0
        type = wxPluralFormsToken::T_SEMICOLON;
321
0
    }
322
0
    else if (*m_s == '(')
323
0
    {
324
0
        ++m_s;
325
0
        type = wxPluralFormsToken::T_LEFT_BRACKET;
326
0
    }
327
0
    else if (*m_s == ')')
328
0
    {
329
0
        ++m_s;
330
0
        type = wxPluralFormsToken::T_RIGHT_BRACKET;
331
0
    }
332
0
    m_token.setType(type);
333
0
    return type != wxPluralFormsToken::T_ERROR;
334
0
}
335
336
class wxPluralFormsNode;
337
338
using wxPluralFormsNodePtr = std::unique_ptr<wxPluralFormsNode>;
339
340
class wxPluralFormsNode
341
{
342
public:
343
0
    wxPluralFormsNode(const wxPluralFormsToken& t) : m_token(t) {}
344
0
    const wxPluralFormsToken& token() const { return m_token; }
345
    const wxPluralFormsNode* node(unsigned i) const
346
0
        { return m_nodes[i].get(); }
347
    void setNode(unsigned i, wxPluralFormsNode* n);
348
    wxPluralFormsNode* releaseNode(unsigned i);
349
    wxPluralFormsToken::Number evaluate(wxPluralFormsToken::Number n) const;
350
351
private:
352
    wxPluralFormsToken m_token;
353
    wxPluralFormsNodePtr m_nodes[3];
354
};
355
356
void wxPluralFormsNode::setNode(unsigned i, wxPluralFormsNode* n)
357
0
{
358
0
    m_nodes[i].reset(n);
359
0
}
360
361
wxPluralFormsNode*  wxPluralFormsNode::releaseNode(unsigned i)
362
0
{
363
0
    return m_nodes[i].release();
364
0
}
365
366
wxPluralFormsToken::Number
367
wxPluralFormsNode::evaluate(wxPluralFormsToken::Number n) const
368
0
{
369
0
    switch (token().type())
370
0
    {
371
        // leaf
372
0
        case wxPluralFormsToken::T_NUMBER:
373
0
            return token().number();
374
0
        case wxPluralFormsToken::T_N:
375
0
            return n;
376
        // 2 args
377
0
        case wxPluralFormsToken::T_EQUAL:
378
0
            return node(0)->evaluate(n) == node(1)->evaluate(n);
379
0
        case wxPluralFormsToken::T_NOT_EQUAL:
380
0
            return node(0)->evaluate(n) != node(1)->evaluate(n);
381
0
        case wxPluralFormsToken::T_GREATER:
382
0
            return node(0)->evaluate(n) > node(1)->evaluate(n);
383
0
        case wxPluralFormsToken::T_GREATER_OR_EQUAL:
384
0
            return node(0)->evaluate(n) >= node(1)->evaluate(n);
385
0
        case wxPluralFormsToken::T_LESS:
386
0
            return node(0)->evaluate(n) < node(1)->evaluate(n);
387
0
        case wxPluralFormsToken::T_LESS_OR_EQUAL:
388
0
            return node(0)->evaluate(n) <= node(1)->evaluate(n);
389
0
        case wxPluralFormsToken::T_REMINDER:
390
0
            {
391
0
                wxPluralFormsToken::Number number = node(1)->evaluate(n);
392
0
                if (number != 0)
393
0
                {
394
0
                    return node(0)->evaluate(n) % number;
395
0
                }
396
0
                else
397
0
                {
398
0
                    return 0;
399
0
                }
400
0
            }
401
0
        case wxPluralFormsToken::T_LOGICAL_AND:
402
0
            return node(0)->evaluate(n) && node(1)->evaluate(n);
403
0
        case wxPluralFormsToken::T_LOGICAL_OR:
404
0
            return node(0)->evaluate(n) || node(1)->evaluate(n);
405
        // 3 args
406
0
        case wxPluralFormsToken::T_QUESTION:
407
0
            return node(0)->evaluate(n)
408
0
                ? node(1)->evaluate(n)
409
0
                : node(2)->evaluate(n);
410
0
        default:
411
0
            return 0;
412
0
    }
413
0
}
414
415
416
class wxPluralFormsCalculator
417
{
418
public:
419
0
    wxPluralFormsCalculator() : m_nplurals(0), m_plural(nullptr) {}
420
421
    // input: number, returns msgstr index
422
    int evaluate(int n) const;
423
424
    // input: text after "Plural-Forms:" (e.g. "nplurals=2; plural=(n != 1);"),
425
    // if s == 0, creates default handler
426
    // returns 0 if error
427
    static wxPluralFormsCalculator* make(const char* s = nullptr);
428
429
0
    ~wxPluralFormsCalculator() {}
430
431
    void  init(wxPluralFormsToken::Number nplurals, wxPluralFormsNode* plural);
432
433
private:
434
    wxPluralFormsToken::Number m_nplurals;
435
    wxPluralFormsNodePtr m_plural;
436
};
437
438
void wxPluralFormsCalculator::init(wxPluralFormsToken::Number nplurals,
439
                                wxPluralFormsNode* plural)
440
0
{
441
0
    m_nplurals = nplurals;
442
0
    m_plural.reset(plural);
443
0
}
444
445
int wxPluralFormsCalculator::evaluate(int n) const
446
0
{
447
0
    if (m_plural.get() == nullptr)
448
0
    {
449
0
        return 0;
450
0
    }
451
0
    wxPluralFormsToken::Number number = m_plural->evaluate(n);
452
0
    if (number < 0 || number > m_nplurals)
453
0
    {
454
0
        return 0;
455
0
    }
456
0
    return number;
457
0
}
458
459
460
class wxPluralFormsParser
461
{
462
public:
463
0
    wxPluralFormsParser(wxPluralFormsScanner& scanner) : m_scanner(scanner) {}
464
    bool parse(wxPluralFormsCalculator& rCalculator);
465
466
private:
467
    wxPluralFormsNode* parsePlural();
468
    // stops at T_SEMICOLON, returns 0 if error
469
    wxPluralFormsScanner& m_scanner;
470
    const wxPluralFormsToken& token() const;
471
    bool nextToken();
472
473
    wxPluralFormsNode* expression();
474
    wxPluralFormsNode* logicalOrExpression();
475
    wxPluralFormsNode* logicalAndExpression();
476
    wxPluralFormsNode* equalityExpression();
477
    wxPluralFormsNode* multiplicativeExpression();
478
    wxPluralFormsNode* relationalExpression();
479
    wxPluralFormsNode* pmExpression();
480
};
481
482
bool wxPluralFormsParser::parse(wxPluralFormsCalculator& rCalculator)
483
0
{
484
0
    if (token().type() != wxPluralFormsToken::T_NPLURALS)
485
0
        return false;
486
0
    if (!nextToken())
487
0
        return false;
488
0
    if (token().type() != wxPluralFormsToken::T_ASSIGN)
489
0
        return false;
490
0
    if (!nextToken())
491
0
        return false;
492
0
    if (token().type() != wxPluralFormsToken::T_NUMBER)
493
0
        return false;
494
0
    wxPluralFormsToken::Number nplurals = token().number();
495
0
    if (!nextToken())
496
0
        return false;
497
0
    if (token().type() != wxPluralFormsToken::T_SEMICOLON)
498
0
        return false;
499
0
    if (!nextToken())
500
0
        return false;
501
0
    if (token().type() != wxPluralFormsToken::T_PLURAL)
502
0
        return false;
503
0
    if (!nextToken())
504
0
        return false;
505
0
    if (token().type() != wxPluralFormsToken::T_ASSIGN)
506
0
        return false;
507
0
    if (!nextToken())
508
0
        return false;
509
0
    wxPluralFormsNode* plural = parsePlural();
510
0
    if (plural == nullptr)
511
0
        return false;
512
0
    if (token().type() != wxPluralFormsToken::T_SEMICOLON)
513
0
        return false;
514
0
    if (!nextToken())
515
0
        return false;
516
0
    if (token().type() != wxPluralFormsToken::T_EOF)
517
0
        return false;
518
0
    rCalculator.init(nplurals, plural);
519
0
    return true;
520
0
}
521
522
wxPluralFormsNode* wxPluralFormsParser::parsePlural()
523
0
{
524
0
    wxPluralFormsNode* p = expression();
525
0
    if (p == nullptr)
526
0
    {
527
0
        return nullptr;
528
0
    }
529
0
    wxPluralFormsNodePtr n(p);
530
0
    if (token().type() != wxPluralFormsToken::T_SEMICOLON)
531
0
    {
532
0
        return nullptr;
533
0
    }
534
0
    return n.release();
535
0
}
536
537
const wxPluralFormsToken& wxPluralFormsParser::token() const
538
0
{
539
0
    return m_scanner.token();
540
0
}
541
542
bool wxPluralFormsParser::nextToken()
543
0
{
544
0
    if (!m_scanner.nextToken())
545
0
        return false;
546
0
    return true;
547
0
}
548
549
wxPluralFormsNode* wxPluralFormsParser::expression()
550
0
{
551
0
    wxPluralFormsNode* p = logicalOrExpression();
552
0
    if (p == nullptr)
553
0
        return nullptr;
554
0
    wxPluralFormsNodePtr n(p);
555
0
    if (token().type() == wxPluralFormsToken::T_QUESTION)
556
0
    {
557
0
        wxPluralFormsNodePtr qn(new wxPluralFormsNode(token()));
558
0
        if (!nextToken())
559
0
        {
560
0
            return nullptr;
561
0
        }
562
0
        p = expression();
563
0
        if (p == nullptr)
564
0
        {
565
0
            return nullptr;
566
0
        }
567
0
        qn->setNode(1, p);
568
0
        if (token().type() != wxPluralFormsToken::T_COLON)
569
0
        {
570
0
            return nullptr;
571
0
        }
572
0
        if (!nextToken())
573
0
        {
574
0
            return nullptr;
575
0
        }
576
0
        p = expression();
577
0
        if (p == nullptr)
578
0
        {
579
0
            return nullptr;
580
0
        }
581
0
        qn->setNode(2, p);
582
0
        qn->setNode(0, n.release());
583
0
        return qn.release();
584
0
    }
585
0
    return n.release();
586
0
}
587
588
wxPluralFormsNode*wxPluralFormsParser::logicalOrExpression()
589
0
{
590
0
    wxPluralFormsNode* p = logicalAndExpression();
591
0
    if (p == nullptr)
592
0
        return nullptr;
593
0
    wxPluralFormsNodePtr ln(p);
594
0
    if (token().type() == wxPluralFormsToken::T_LOGICAL_OR)
595
0
    {
596
0
        wxPluralFormsNodePtr un(new wxPluralFormsNode(token()));
597
0
        if (!nextToken())
598
0
        {
599
0
            return nullptr;
600
0
        }
601
0
        p = logicalOrExpression();
602
0
        if (p == nullptr)
603
0
        {
604
0
            return nullptr;
605
0
        }
606
0
        wxPluralFormsNodePtr rn(p);    // right
607
0
        if (rn->token().type() == wxPluralFormsToken::T_LOGICAL_OR)
608
0
        {
609
            // see logicalAndExpression comment
610
0
            un->setNode(0, ln.release());
611
0
            un->setNode(1, rn->releaseNode(0));
612
0
            rn->setNode(0, un.release());
613
0
            return rn.release();
614
0
        }
615
616
617
0
        un->setNode(0, ln.release());
618
0
        un->setNode(1, rn.release());
619
0
        return un.release();
620
0
    }
621
0
    return ln.release();
622
0
}
623
624
wxPluralFormsNode* wxPluralFormsParser::logicalAndExpression()
625
0
{
626
0
    wxPluralFormsNode* p = equalityExpression();
627
0
    if (p == nullptr)
628
0
        return nullptr;
629
0
    wxPluralFormsNodePtr ln(p);   // left
630
0
    if (token().type() == wxPluralFormsToken::T_LOGICAL_AND)
631
0
    {
632
0
        wxPluralFormsNodePtr un(new wxPluralFormsNode(token()));  // up
633
0
        if (!nextToken())
634
0
        {
635
0
            return nullptr;
636
0
        }
637
0
        p = logicalAndExpression();
638
0
        if (p == nullptr)
639
0
        {
640
0
            return nullptr;
641
0
        }
642
0
        wxPluralFormsNodePtr rn(p);    // right
643
0
        if (rn->token().type() == wxPluralFormsToken::T_LOGICAL_AND)
644
0
        {
645
// transform 1 && (2 && 3) -> (1 && 2) && 3
646
//     u                  r
647
// l       r     ->   u      3
648
//       2   3      l   2
649
0
            un->setNode(0, ln.release());
650
0
            un->setNode(1, rn->releaseNode(0));
651
0
            rn->setNode(0, un.release());
652
0
            return rn.release();
653
0
        }
654
655
0
        un->setNode(0, ln.release());
656
0
        un->setNode(1, rn.release());
657
0
        return un.release();
658
0
    }
659
0
    return ln.release();
660
0
}
661
662
wxPluralFormsNode* wxPluralFormsParser::equalityExpression()
663
0
{
664
0
    wxPluralFormsNode* p = relationalExpression();
665
0
    if (p == nullptr)
666
0
        return nullptr;
667
0
    wxPluralFormsNodePtr n(p);
668
0
    if (token().type() == wxPluralFormsToken::T_EQUAL
669
0
        || token().type() == wxPluralFormsToken::T_NOT_EQUAL)
670
0
    {
671
0
        wxPluralFormsNodePtr qn(new wxPluralFormsNode(token()));
672
0
        if (!nextToken())
673
0
        {
674
0
            return nullptr;
675
0
        }
676
0
        p = relationalExpression();
677
0
        if (p == nullptr)
678
0
        {
679
0
            return nullptr;
680
0
        }
681
0
        qn->setNode(1, p);
682
0
        qn->setNode(0, n.release());
683
0
        return qn.release();
684
0
    }
685
0
    return n.release();
686
0
}
687
688
wxPluralFormsNode* wxPluralFormsParser::relationalExpression()
689
0
{
690
0
    wxPluralFormsNode* p = multiplicativeExpression();
691
0
    if (p == nullptr)
692
0
        return nullptr;
693
0
    wxPluralFormsNodePtr n(p);
694
0
    if (token().type() == wxPluralFormsToken::T_GREATER
695
0
            || token().type() == wxPluralFormsToken::T_LESS
696
0
            || token().type() == wxPluralFormsToken::T_GREATER_OR_EQUAL
697
0
            || token().type() == wxPluralFormsToken::T_LESS_OR_EQUAL)
698
0
    {
699
0
        wxPluralFormsNodePtr qn(new wxPluralFormsNode(token()));
700
0
        if (!nextToken())
701
0
        {
702
0
            return nullptr;
703
0
        }
704
0
        p = multiplicativeExpression();
705
0
        if (p == nullptr)
706
0
        {
707
0
            return nullptr;
708
0
        }
709
0
        qn->setNode(1, p);
710
0
        qn->setNode(0, n.release());
711
0
        return qn.release();
712
0
    }
713
0
    return n.release();
714
0
}
715
716
wxPluralFormsNode* wxPluralFormsParser::multiplicativeExpression()
717
0
{
718
0
    wxPluralFormsNode* p = pmExpression();
719
0
    if (p == nullptr)
720
0
        return nullptr;
721
0
    wxPluralFormsNodePtr n(p);
722
0
    if (token().type() == wxPluralFormsToken::T_REMINDER)
723
0
    {
724
0
        wxPluralFormsNodePtr qn(new wxPluralFormsNode(token()));
725
0
        if (!nextToken())
726
0
        {
727
0
            return nullptr;
728
0
        }
729
0
        p = pmExpression();
730
0
        if (p == nullptr)
731
0
        {
732
0
            return nullptr;
733
0
        }
734
0
        qn->setNode(1, p);
735
0
        qn->setNode(0, n.release());
736
0
        return qn.release();
737
0
    }
738
0
    return n.release();
739
0
}
740
741
wxPluralFormsNode* wxPluralFormsParser::pmExpression()
742
0
{
743
0
    wxPluralFormsNodePtr n;
744
0
    if (token().type() == wxPluralFormsToken::T_N
745
0
        || token().type() == wxPluralFormsToken::T_NUMBER)
746
0
    {
747
0
        n.reset(new wxPluralFormsNode(token()));
748
0
        if (!nextToken())
749
0
        {
750
0
            return nullptr;
751
0
        }
752
0
    }
753
0
    else if (token().type() == wxPluralFormsToken::T_LEFT_BRACKET) {
754
0
        if (!nextToken())
755
0
        {
756
0
            return nullptr;
757
0
        }
758
0
        wxPluralFormsNode* p = expression();
759
0
        if (p == nullptr)
760
0
        {
761
0
            return nullptr;
762
0
        }
763
0
        n.reset(p);
764
0
        if (token().type() != wxPluralFormsToken::T_RIGHT_BRACKET)
765
0
        {
766
0
            return nullptr;
767
0
        }
768
0
        if (!nextToken())
769
0
        {
770
0
            return nullptr;
771
0
        }
772
0
    }
773
0
    else
774
0
    {
775
0
        return nullptr;
776
0
    }
777
0
    return n.release();
778
0
}
779
780
wxPluralFormsCalculator* wxPluralFormsCalculator::make(const char* s)
781
0
{
782
0
    wxPluralFormsCalculatorPtr calculator(new wxPluralFormsCalculator);
783
0
    if (s != nullptr)
784
0
    {
785
0
        wxPluralFormsScanner scanner(s);
786
0
        wxPluralFormsParser p(scanner);
787
0
        if (!p.parse(*calculator))
788
0
        {
789
0
            return nullptr;
790
0
        }
791
0
    }
792
0
    return calculator.release();
793
0
}
794
795
796
797
798
// ----------------------------------------------------------------------------
799
// wxMsgCatalogFile corresponds to one disk-file message catalog.
800
//
801
// This is a "low-level" class and is used only by wxMsgCatalog
802
// NOTE: for the documentation of the binary catalog (.MO) files refer to
803
//       the GNU gettext manual:
804
//       http://www.gnu.org/software/autoconf/manual/gettext/MO-Files.html
805
// ----------------------------------------------------------------------------
806
807
class wxMsgCatalogFile
808
{
809
public:
810
    typedef wxScopedCharBuffer DataBuffer;
811
812
    // ctor & dtor
813
    wxMsgCatalogFile();
814
    ~wxMsgCatalogFile();
815
816
    // load the catalog from disk
817
    bool LoadFile(const wxString& filename,
818
                  wxPluralFormsCalculatorPtr& rPluralFormsCalculator);
819
    bool LoadData(const DataBuffer& data,
820
                  wxPluralFormsCalculatorPtr& rPluralFormsCalculator);
821
822
    // fills the hash with string-translation pairs
823
    bool FillHash(wxTranslationsHashMap& hash, const wxString& domain) const;
824
825
    // return the charset of the strings in this catalog or empty string if
826
    // none/unknown
827
0
    wxString GetCharset() const { return m_charset; }
828
829
private:
830
    // this implementation is binary compatible with GNU gettext() version 0.10
831
832
    // an entry in the string table
833
    struct wxMsgTableEntry
834
    {
835
        size_t32   nLen;           // length of the string
836
        size_t32   ofsString;      // pointer to the string
837
    };
838
839
    // header of a .mo file
840
    struct wxMsgCatalogHeader
841
    {
842
        size_t32  magic,          // offset +00:  magic id
843
                  revision,       //        +04:  revision
844
                  numStrings;     //        +08:  number of strings in the file
845
        size_t32  ofsOrigTable,   //        +0C:  start of original string table
846
                  ofsTransTable;  //        +10:  start of translated string table
847
        size_t32  nHashSize,      //        +14:  hash table size
848
                  ofsHashTable;   //        +18:  offset of hash table start
849
    };
850
851
    // all data is stored here
852
    DataBuffer m_data;
853
854
    // data description
855
    size_t32          m_numStrings;   // number of strings in this domain
856
    const
857
    wxMsgTableEntry  *m_pOrigTable,   // pointer to original   strings
858
                     *m_pTransTable;  //            translated
859
860
    wxString m_charset;               // from the message catalog header
861
862
863
    // swap the 2 halves of 32 bit integer if needed
864
    size_t32 Swap(size_t32 ui) const
865
0
    {
866
0
        return m_bSwapped ? (ui << 24) | ((ui & 0xff00) << 8) |
867
0
                            ((ui >> 8) & 0xff00) | (ui >> 24)
868
0
                            : ui;
869
0
    }
870
871
    const char* StringAtOfs(const wxMsgTableEntry* pTable, size_t32 n) const
872
0
    {
873
0
        const wxMsgTableEntry * const ent = pTable + n;
874
875
        // this check could fail for a corrupt message catalog
876
0
        size_t32 ofsString = Swap(ent->ofsString);
877
0
        if ( ofsString + Swap(ent->nLen) > m_data.length())
878
0
        {
879
0
            return nullptr;
880
0
        }
881
882
0
        return m_data.data() + ofsString;
883
0
    }
884
885
    bool m_bSwapped;   // wrong endianness?
886
887
    wxDECLARE_NO_COPY_CLASS(wxMsgCatalogFile);
888
};
889
890
// ----------------------------------------------------------------------------
891
// wxMsgCatalogFile class
892
// ----------------------------------------------------------------------------
893
894
wxMsgCatalogFile::wxMsgCatalogFile()
895
0
{
896
0
}
897
898
wxMsgCatalogFile::~wxMsgCatalogFile()
899
0
{
900
0
}
901
902
// open disk file and read in its contents
903
bool wxMsgCatalogFile::LoadFile(const wxString& filename,
904
                                wxPluralFormsCalculatorPtr& rPluralFormsCalculator)
905
0
{
906
0
    wxFile fileMsg(filename);
907
0
    if ( !fileMsg.IsOpened() )
908
0
        return false;
909
910
    // get the file size (assume it is less than 4GB...)
911
0
    wxFileOffset lenFile = fileMsg.Length();
912
0
    if ( lenFile == wxInvalidOffset )
913
0
        return false;
914
915
0
    size_t nSize = wx_truncate_cast(size_t, lenFile);
916
0
    wxASSERT_MSG( nSize == lenFile + size_t(0), wxS("message catalog bigger than 4GB?") );
917
918
0
    wxMemoryBuffer filedata;
919
920
    // read the whole file in memory
921
0
    if ( fileMsg.Read(filedata.GetWriteBuf(nSize), nSize) != lenFile )
922
0
        return false;
923
924
0
    filedata.UngetWriteBuf(nSize);
925
926
0
    bool ok = LoadData
927
0
              (
928
0
                  DataBuffer::CreateOwned((char*)filedata.release(), nSize),
929
0
                  rPluralFormsCalculator
930
0
              );
931
0
    if ( !ok )
932
0
    {
933
0
        wxLogWarning(_("'%s' is not a valid message catalog."), filename);
934
0
        return false;
935
0
    }
936
937
0
    return true;
938
0
}
939
940
941
bool wxMsgCatalogFile::LoadData(const DataBuffer& data,
942
                                wxPluralFormsCalculatorPtr& rPluralFormsCalculator)
943
0
{
944
    // examine header
945
0
    bool bValid = data.length() > sizeof(wxMsgCatalogHeader);
946
947
0
    const wxMsgCatalogHeader* pHeader = reinterpret_cast<const wxMsgCatalogHeader*>(data.data());
948
0
    if ( bValid ) {
949
        // we'll have to swap all the integers if it's true
950
0
        m_bSwapped = pHeader->magic == MSGCATALOG_MAGIC_SW;
951
952
        // check the magic number
953
0
        bValid = m_bSwapped || pHeader->magic == MSGCATALOG_MAGIC;
954
0
    }
955
956
0
    if ( !bValid ) {
957
        // it's either too short or has incorrect magic number
958
0
        wxLogWarning(_("Invalid message catalog."));
959
0
        return false;
960
0
    }
961
962
0
    m_data = data;
963
964
    // initialize
965
0
    m_numStrings  = Swap(pHeader->numStrings);
966
0
    m_pOrigTable  = reinterpret_cast<const wxMsgTableEntry*>(data.data() +
967
0
                    Swap(pHeader->ofsOrigTable));
968
0
    m_pTransTable = reinterpret_cast<const wxMsgTableEntry*>(data.data() +
969
0
                    Swap(pHeader->ofsTransTable));
970
971
    // now parse catalog's header and try to extract catalog charset and
972
    // plural forms formula from it:
973
974
0
    const char* headerData = StringAtOfs(m_pOrigTable, 0);
975
0
    if ( headerData && headerData[0] == '\0' )
976
0
    {
977
        // Extract the charset:
978
0
        const char * const header = StringAtOfs(m_pTransTable, 0);
979
0
        const char *
980
0
            cset = strstr(header, "Content-Type: text/plain; charset=");
981
0
        if ( cset )
982
0
        {
983
0
            cset += 34; // strlen("Content-Type: text/plain; charset=")
984
985
0
            const char * const csetEnd = strchr(cset, '\n');
986
0
            if ( csetEnd )
987
0
            {
988
0
                m_charset = wxString(cset, csetEnd - cset);
989
0
                if ( m_charset == wxS("CHARSET") )
990
0
                {
991
                    // "CHARSET" is not valid charset, but lazy translator
992
0
                    m_charset.clear();
993
0
                }
994
0
            }
995
0
        }
996
        // else: incorrectly filled Content-Type header
997
998
        // Extract plural forms:
999
0
        const char * plurals = strstr(header, "Plural-Forms:");
1000
0
        if ( plurals )
1001
0
        {
1002
0
            plurals += 13; // strlen("Plural-Forms:")
1003
0
            const char * const pluralsEnd = strchr(plurals, '\n');
1004
0
            if ( pluralsEnd )
1005
0
            {
1006
0
                const size_t pluralsLen = pluralsEnd - plurals;
1007
0
                wxCharBuffer buf(pluralsLen);
1008
0
                strncpy(buf.data(), plurals, pluralsLen);
1009
0
                wxPluralFormsCalculator * const
1010
0
                    pCalculator = wxPluralFormsCalculator::make(buf);
1011
0
                if ( pCalculator )
1012
0
                {
1013
0
                    rPluralFormsCalculator.reset(pCalculator);
1014
0
                }
1015
0
                else
1016
0
                {
1017
0
                    wxLogVerbose(_("Failed to parse Plural-Forms: '%s'"),
1018
0
                                 buf.data());
1019
0
                }
1020
0
            }
1021
0
        }
1022
1023
0
        if ( !rPluralFormsCalculator.get() )
1024
0
            rPluralFormsCalculator.reset(wxPluralFormsCalculator::make());
1025
0
    }
1026
1027
    // everything is fine
1028
0
    return true;
1029
0
}
1030
1031
bool wxMsgCatalogFile::FillHash(wxTranslationsHashMap& hash,
1032
                                const wxString& domain) const
1033
0
{
1034
0
    wxUnusedVar(domain); // silence warning in Unicode build
1035
1036
    // conversion to use to convert catalog strings to the GUI encoding
1037
0
    wxMBConv *inputConv = nullptr;
1038
1039
0
    std::unique_ptr<wxMBConv> inputConvPtr; // just to delete inputConv if needed
1040
1041
0
    if ( !m_charset.empty() )
1042
0
    {
1043
0
        inputConv = new wxCSConv(m_charset);
1044
1045
        // As we allocated it ourselves, we need to delete it, so ensure
1046
        // this happens.
1047
0
        inputConvPtr.reset(inputConv);
1048
0
    }
1049
0
    else // no need to convert the encoding
1050
0
    {
1051
        // we must somehow convert the narrow strings in the message catalog to
1052
        // wide strings, so use the default conversion if we have no charset
1053
0
        inputConv = wxConvCurrent;
1054
0
    }
1055
1056
0
    for (size_t32 i = 0; i < m_numStrings; i++)
1057
0
    {
1058
0
        const char *data = StringAtOfs(m_pOrigTable, i);
1059
0
        if (!data)
1060
0
            return false; // may happen for invalid MO files
1061
1062
0
        wxString msgid;
1063
0
        msgid = wxString(data, *inputConv);
1064
0
        data = StringAtOfs(m_pTransTable, i);
1065
0
        if (!data)
1066
0
            return false; // may happen for invalid MO files
1067
1068
0
        size_t length = Swap(m_pTransTable[i].nLen);
1069
0
        size_t offset = 0;
1070
0
        size_t index = 0;
1071
0
        while (offset < length)
1072
0
        {
1073
0
            const char * const str = data + offset;
1074
1075
0
            wxString msgstr;
1076
0
            msgstr = wxString(str, *inputConv);
1077
0
            if ( !msgstr.empty() )
1078
0
            {
1079
0
                hash[index == 0 ? msgid : msgid + wxChar(index)] = msgstr;
1080
0
            }
1081
1082
            // skip this string
1083
            // IMPORTANT: accesses to the 'data' pointer are valid only for
1084
            //            the first 'length+1' bytes (GNU specs says that the
1085
            //            final NUL is not counted in length); using wxStrnlen()
1086
            //            we make sure we don't access memory beyond the valid range
1087
            //            (which otherwise may happen for invalid MO files):
1088
0
            offset += wxStrnlen(str, length - offset) + 1;
1089
0
            ++index;
1090
0
        }
1091
0
    }
1092
1093
0
    return true;
1094
0
}
1095
1096
1097
// ----------------------------------------------------------------------------
1098
// wxMsgCatalog class
1099
// ----------------------------------------------------------------------------
1100
1101
wxMsgCatalog::wxMsgCatalog(const wxString& domain)
1102
0
    : m_pNext(nullptr), m_domain(domain)
1103
0
{
1104
0
}
1105
1106
0
wxMsgCatalog::~wxMsgCatalog() = default;
1107
1108
/* static */
1109
wxMsgCatalog *wxMsgCatalog::CreateFromFile(const wxString& filename,
1110
                                           const wxString& domain)
1111
0
{
1112
0
    std::unique_ptr<wxMsgCatalog> cat(new wxMsgCatalog(domain));
1113
1114
0
    wxMsgCatalogFile file;
1115
1116
0
    if ( !file.LoadFile(filename, cat->m_pluralFormsCalculator) )
1117
0
        return nullptr;
1118
1119
0
    if ( !file.FillHash(cat->m_messages, domain) )
1120
0
        return nullptr;
1121
1122
0
    return cat.release();
1123
0
}
1124
1125
/* static */
1126
wxMsgCatalog *wxMsgCatalog::CreateFromData(const wxScopedCharBuffer& data,
1127
                                           const wxString& domain)
1128
0
{
1129
0
    std::unique_ptr<wxMsgCatalog> cat(new wxMsgCatalog(domain));
1130
1131
0
    wxMsgCatalogFile file;
1132
1133
0
    if ( !file.LoadData(data, cat->m_pluralFormsCalculator) )
1134
0
        return nullptr;
1135
1136
0
    if ( !file.FillHash(cat->m_messages, domain) )
1137
0
        return nullptr;
1138
1139
0
    return cat.release();
1140
0
}
1141
1142
const wxString *wxMsgCatalog::GetString(const wxString& str, unsigned n, const wxString& context) const
1143
0
{
1144
0
    int index = 0;
1145
0
    if (n != UINT_MAX)
1146
0
    {
1147
0
        index = m_pluralFormsCalculator->evaluate(n);
1148
0
    }
1149
0
    wxTranslationsHashMap::const_iterator i;
1150
0
    if (index != 0)
1151
0
    {
1152
0
        if (context.IsEmpty())
1153
0
            i = m_messages.find(wxString(str) + wxChar(index));   // plural, no context
1154
0
        else
1155
0
            i = m_messages.find(wxString(context) + wxString('\x04') + wxString(str) + wxChar(index));   // plural, context
1156
0
    }
1157
0
    else
1158
0
    {
1159
0
        if (context.IsEmpty())
1160
0
            i = m_messages.find(str); // no context
1161
0
        else
1162
0
            i = m_messages.find(wxString(context) + wxString('\x04') + wxString(str)); // context
1163
0
    }
1164
1165
0
    if ( i != m_messages.end() )
1166
0
    {
1167
0
        return &i->second;
1168
0
    }
1169
0
    else
1170
0
        return nullptr;
1171
0
}
1172
1173
1174
// ----------------------------------------------------------------------------
1175
// wxTranslations
1176
// ----------------------------------------------------------------------------
1177
1178
namespace
1179
{
1180
1181
wxTranslations *gs_translations = nullptr;
1182
bool gs_translationsOwned = false;
1183
1184
} // anonymous namespace
1185
1186
1187
/*static*/
1188
wxTranslations *wxTranslations::Get()
1189
0
{
1190
0
    return gs_translations;
1191
0
}
1192
1193
/*static*/
1194
void wxTranslations::Set(wxTranslations *t)
1195
0
{
1196
0
    if ( gs_translationsOwned )
1197
0
        delete gs_translations;
1198
0
    gs_translations = t;
1199
0
    gs_translationsOwned = true;
1200
0
}
1201
1202
/*static*/
1203
void wxTranslations::SetNonOwned(wxTranslations *t)
1204
0
{
1205
0
    if ( gs_translationsOwned )
1206
0
        delete gs_translations;
1207
0
    gs_translations = t;
1208
0
    gs_translationsOwned = false;
1209
0
}
1210
1211
1212
wxTranslations::wxTranslations()
1213
0
{
1214
0
    m_pMsgCat = nullptr;
1215
0
    m_loader = new wxFileTranslationsLoader;
1216
0
}
1217
1218
1219
wxTranslations::~wxTranslations()
1220
0
{
1221
0
    delete m_loader;
1222
1223
    // free catalogs memory
1224
0
    while ( m_pMsgCat != nullptr )
1225
0
    {
1226
0
        wxMsgCatalog* pTmpCat;
1227
0
        pTmpCat = m_pMsgCat;
1228
0
        m_pMsgCat = m_pMsgCat->m_pNext;
1229
0
        delete pTmpCat;
1230
0
    }
1231
0
}
1232
1233
1234
void wxTranslations::SetLoader(wxTranslationsLoader *loader)
1235
0
{
1236
0
    wxCHECK_RET( loader, "loader can't be null" );
1237
1238
0
    delete m_loader;
1239
0
    m_loader = loader;
1240
0
}
1241
1242
1243
void wxTranslations::SetLanguage(wxLanguage lang)
1244
0
{
1245
0
    if ( lang == wxLANGUAGE_DEFAULT )
1246
0
        SetLanguage(wxString());
1247
0
    else
1248
0
        SetLanguage(wxUILocale::GetLanguageCanonicalName(lang));
1249
0
}
1250
1251
void wxTranslations::SetLanguage(const wxString& lang)
1252
0
{
1253
0
    m_lang = lang;
1254
0
}
1255
1256
1257
wxArrayString wxTranslations::GetAvailableTranslations(const wxString& domain) const
1258
0
{
1259
0
    wxCHECK_MSG( m_loader, wxArrayString(), "loader can't be null" );
1260
1261
0
    return m_loader->GetAvailableTranslations(domain);
1262
0
}
1263
1264
1265
bool wxTranslations::AddStdCatalog()
1266
0
{
1267
    // Try loading the message catalog for this version first, but fall back to
1268
    // the name without the version if it's not found, as message catalogs
1269
    // typically won't have the version in their names under non-Unix platforms
1270
    // (i.e. where they're not installed by our own "make install").
1271
0
    wxString domain("wxstd-" wxSTRINGIZE(wxMAJOR_VERSION) "." wxSTRINGIZE(wxMINOR_VERSION));
1272
0
    if ( GetBestAvailableTranslation(domain).empty() )
1273
0
        domain = wxS("wxstd");
1274
1275
0
    return AddCatalog(domain);
1276
0
}
1277
1278
bool wxTranslations::AddAvailableCatalog(const wxString& domain, wxLanguage msgIdLanguage)
1279
0
{
1280
0
    return DoAddCatalog(domain, msgIdLanguage) == Translations::Found;
1281
0
}
1282
1283
bool wxTranslations::AddCatalog(const wxString& domain, wxLanguage msgIdLanguage)
1284
0
{
1285
0
    return DoAddCatalog(domain, msgIdLanguage) != Translations::NotFound;
1286
0
}
1287
1288
wxTranslations::Translations wxTranslations::DoAddCatalog(const wxString& domain,
1289
                                                          wxLanguage msgIdLanguage)
1290
0
{
1291
0
    const wxString msgIdLang = wxUILocale::GetLanguageCanonicalName(msgIdLanguage);
1292
0
    const wxString domain_lang = GetBestTranslation(domain, msgIdLang);
1293
0
    if ( domain_lang.empty() )
1294
0
    {
1295
0
        wxLogTrace(TRACE_I18N,
1296
0
                    wxS("no suitable translation for domain '%s' found"),
1297
0
                    domain);
1298
0
        return Translations::NotFound;
1299
0
    }
1300
1301
0
    if ( LoadCatalog(domain, domain_lang) )
1302
0
    {
1303
0
        wxLogTrace(TRACE_I18N,
1304
0
                   wxS("adding '%s' translation for domain '%s' (msgid language '%s')"),
1305
0
                   domain_lang, domain, msgIdLang);
1306
0
        return Translations::Found;
1307
0
    }
1308
1309
    // LoadCatalog() failed, but GetBestTranslation() returned non-empty language.
1310
    // That must mean that msgIdLanguage was used.
1311
0
    wxLogTrace(TRACE_I18N,
1312
0
                wxS("not using translations for domain '%s' with msgid language '%s'"),
1313
0
                domain, msgIdLang);
1314
0
    return Translations::NotNeeded;
1315
0
}
1316
1317
1318
bool wxTranslations::LoadCatalog(const wxString& domain, const wxString& lang)
1319
0
{
1320
0
    wxCHECK_MSG( m_loader, false, "loader can't be null" );
1321
1322
0
    wxMsgCatalog *cat = nullptr;
1323
1324
0
#if wxUSE_FONTMAP
1325
    // first look for the catalog for this language and the current locale:
1326
    // notice that we don't use the system name for the locale as this would
1327
    // force us to install catalogs in different locations depending on the
1328
    // system but always use the canonical name
1329
0
    wxFontEncoding encSys = wxLocale::GetSystemEncoding();
1330
0
    if ( encSys != wxFONTENCODING_SYSTEM )
1331
0
    {
1332
0
        wxString fullname(lang);
1333
0
        fullname << wxS('.') << wxFontMapperBase::GetEncodingName(encSys);
1334
1335
0
        cat = m_loader->LoadCatalog(domain, fullname);
1336
0
    }
1337
0
#endif // wxUSE_FONTMAP
1338
1339
0
    if ( !cat )
1340
0
    {
1341
        // Next try: use the provided name language name:
1342
0
        cat = m_loader->LoadCatalog(domain, lang);
1343
0
    }
1344
1345
0
    if ( !cat )
1346
0
    {
1347
        // Also try just base locale name: for things like "fr_BE" (Belgium
1348
        // French) we should use fall back on plain "fr" if no Belgium-specific
1349
        // message catalogs exist
1350
0
        wxString baselang = lang.BeforeFirst('_');
1351
0
        if ( lang != baselang )
1352
0
            cat = m_loader->LoadCatalog(domain, baselang);
1353
0
    }
1354
1355
0
    if ( cat )
1356
0
    {
1357
        // add it to the head of the list so that in GetString it will
1358
        // be searched before the catalogs added earlier
1359
1360
0
        cat->m_pNext = m_pMsgCat;
1361
0
        m_pMsgCat = cat;
1362
0
        m_catalogMap[domain] = cat;
1363
1364
0
        return true;
1365
0
    }
1366
0
    else
1367
0
    {
1368
        // Nothing worked, the catalog just isn't there
1369
0
        wxLogTrace(TRACE_I18N,
1370
0
                   "Catalog \"%s.mo\" not found for language \"%s\".",
1371
0
                   domain, lang);
1372
0
        return false;
1373
0
    }
1374
0
}
1375
1376
// check if the given catalog is loaded
1377
bool wxTranslations::IsLoaded(const wxString& domain) const
1378
0
{
1379
0
    return FindCatalog(domain) != nullptr;
1380
0
}
1381
1382
wxString wxTranslations::GetBestTranslation(const wxString& domain,
1383
                                            wxLanguage msgIdLanguage)
1384
0
{
1385
0
    const wxString lang = wxUILocale::GetLanguageCanonicalName(msgIdLanguage);
1386
0
    return GetBestTranslation(domain, lang);
1387
0
}
1388
1389
wxString wxTranslations::GetBestTranslation(const wxString& domain,
1390
                                            const wxString& msgIdLanguage)
1391
0
{
1392
    // Determine the best language, including the msgId language, which is always
1393
    // available because it is present in the code:
1394
0
    wxString lang = DoGetBestAvailableTranslation(domain, msgIdLanguage);
1395
1396
0
    if ( lang.empty() )
1397
0
    {
1398
0
        wxLogTrace(TRACE_I18N,
1399
0
                   "no available language for domain '%s'", domain);
1400
0
    }
1401
0
    else if ( lang == msgIdLanguage || lang == msgIdLanguage.BeforeFirst('_') )
1402
0
    {
1403
0
        wxLogTrace(TRACE_I18N,
1404
0
                   "using message ID language '%s' for domain '%s'", lang, domain);
1405
0
    }
1406
1407
0
    return lang;
1408
0
}
1409
1410
wxString wxTranslations::GetBestAvailableTranslation(const wxString& domain)
1411
0
{
1412
    // Determine the best language from the ones with actual translation file:
1413
    // As this function never considers the language of the original messages as being
1414
    // available, pass empty string as message ID language to the helper function.
1415
0
    return DoGetBestAvailableTranslation(domain, wxString());
1416
0
}
1417
1418
wxString wxTranslations::DoGetBestAvailableTranslation(const wxString& domain, const wxString& additionalAvailableLanguage)
1419
0
{
1420
0
    wxArrayString available(GetAvailableTranslations(domain));
1421
0
    if ( !additionalAvailableLanguage.empty() )
1422
0
    {
1423
0
        available.push_back(additionalAvailableLanguage);
1424
0
        available.push_back(additionalAvailableLanguage.BeforeFirst('_'));
1425
0
    }
1426
1427
0
    if ( !m_lang.empty() )
1428
0
    {
1429
0
        wxLogTrace(TRACE_I18N,
1430
0
                   "searching for best translation to %s for domain '%s'",
1431
0
                   m_lang, domain);
1432
1433
0
        wxString lang;
1434
0
        if ( available.Index(m_lang) != wxNOT_FOUND )
1435
0
        {
1436
0
            lang = m_lang;
1437
0
        }
1438
0
        else
1439
0
        {
1440
0
            const wxString baselang = m_lang.BeforeFirst('_');
1441
0
            if ( baselang != m_lang && available.Index(baselang) != wxNOT_FOUND )
1442
0
                lang = baselang;
1443
0
        }
1444
1445
0
        if ( lang.empty() )
1446
0
            wxLogTrace(TRACE_I18N, " => no available translations found");
1447
0
        else
1448
0
            wxLogTrace(TRACE_I18N, " => found '%s'", lang);
1449
1450
0
        return lang;
1451
0
    }
1452
1453
0
    const wxString lang = GetPreferredUILanguage(available.AsVector());
1454
0
    wxLogTrace(TRACE_I18N, " => using language '%s'", lang);
1455
0
    return lang;
1456
0
}
1457
1458
namespace
1459
{
1460
1461
// We use this container to store all strings known not to have translations.
1462
// It is thread-specific to avoid using mutexes for every untranslated string
1463
// access.
1464
using UntranslatedStrings = std::unordered_set<wxString>;
1465
1466
/*
1467
    As of October 2025, MinGW still has a long-standing bug in its thread_local
1468
    variables implementation: their memory is de-allocated *before* their
1469
    destructor is called, see https://github.com/msys2/MINGW-packages/issues/2519
1470
1471
    The UntranslatedStringHolder class works around this issue, by storing data
1472
    in global variables outside of this class and only relying on the dtor to
1473
    be executed when any thread (not necessarily created by wxWidgets) exits to
1474
    ensure that we always perform the required cleanup.
1475
 */
1476
#ifdef __MINGW32__
1477
1478
class UntranslatedStringHolder
1479
{
1480
private:
1481
    static wxCriticalSection ms_criticalSection;
1482
    static std::map<wxThreadIdType, UntranslatedStrings> ms_setsMap;
1483
1484
    // This will be set to point to an element of ms_setsMap.
1485
    UntranslatedStrings* m_holder = nullptr;
1486
1487
public:
1488
    UntranslatedStringHolder() = default;
1489
1490
    const wxString& get(const wxString& str)
1491
    {
1492
        if ( m_holder == nullptr )
1493
        {
1494
            wxCriticalSectionLocker locker(ms_criticalSection);
1495
            m_holder = &ms_setsMap[wxThread::GetCurrentId()];
1496
        }
1497
1498
        return *m_holder->insert(str).first;
1499
    }
1500
1501
    ~UntranslatedStringHolder()
1502
    {
1503
        // This code is run after this object memory has been deallocated so we
1504
        // cannot access any member variables, but we can access global ones.
1505
        wxCriticalSectionLocker locker(ms_criticalSection);
1506
        ms_setsMap.erase(wxThread::GetCurrentId());
1507
    }
1508
1509
    wxDECLARE_NO_COPY_CLASS(UntranslatedStringHolder);
1510
};
1511
1512
wxCriticalSection UntranslatedStringHolder::ms_criticalSection;
1513
1514
std::map<wxThreadIdType, UntranslatedStrings> UntranslatedStringHolder::ms_setsMap;
1515
1516
#else // !__MINGW32__
1517
1518
// When not using MinGW, thread_local variables to work correctly but we still
1519
// define this class, even if it's trivial, to use the same code below.
1520
class UntranslatedStringHolder
1521
{
1522
private:
1523
    UntranslatedStrings m_holder;
1524
1525
public:
1526
0
    UntranslatedStringHolder() = default;
1527
1528
    const wxString& get(const wxString& str)
1529
0
    {
1530
0
        return *m_holder.insert(str).first;
1531
0
    }
1532
1533
    wxDECLARE_NO_COPY_CLASS(UntranslatedStringHolder);
1534
};
1535
1536
#endif // __MINGW32__/!__MINGW32__
1537
1538
} // Anonymous namespace
1539
1540
1541
/* static */
1542
const wxString& wxTranslations::GetUntranslatedString(const wxString& str)
1543
0
{
1544
0
    thread_local UntranslatedStringHolder wxPerThreadStrings;
1545
0
    return wxPerThreadStrings.get(str);
1546
0
}
1547
1548
1549
const wxString *wxTranslations::GetTranslatedString(const wxString& origString,
1550
                                                    const wxString& domain,
1551
                                                    const wxString& context) const
1552
0
{
1553
0
    return GetTranslatedString(origString, UINT_MAX, domain, context);
1554
0
}
1555
1556
const wxString *wxTranslations::GetTranslatedString(const wxString& origString,
1557
                                                    unsigned n,
1558
                                                    const wxString& domain,
1559
                                                    const wxString& context) const
1560
0
{
1561
0
    if ( origString.empty() )
1562
0
        return nullptr;
1563
1564
0
    const wxString *trans = nullptr;
1565
0
    wxMsgCatalog *pMsgCat;
1566
1567
0
    if ( !domain.empty() )
1568
0
    {
1569
0
        pMsgCat = FindCatalog(domain);
1570
1571
        // does the catalog exist?
1572
0
        if ( pMsgCat != nullptr )
1573
0
            trans = pMsgCat->GetString(origString, n, context);
1574
0
    }
1575
0
    else
1576
0
    {
1577
        // search in all domains
1578
0
        for ( pMsgCat = m_pMsgCat; pMsgCat != nullptr; pMsgCat = pMsgCat->m_pNext )
1579
0
        {
1580
0
            trans = pMsgCat->GetString(origString, n, context);
1581
0
            if ( trans != nullptr )   // take the first found
1582
0
                break;
1583
0
        }
1584
0
    }
1585
1586
0
    if ( trans == nullptr )
1587
0
    {
1588
0
        wxLogTrace
1589
0
        (
1590
0
            TRACE_I18N,
1591
0
            "string \"%s\"%s not found in %s%slocale '%s'.",
1592
0
            origString,
1593
0
            (n != UINT_MAX ? wxString::Format("[%ld]", (long)n) : wxString()),
1594
0
            (!domain.empty() ? wxString::Format("domain '%s' ", domain) : wxString()),
1595
0
            (!context.empty() ? wxString::Format("context '%s' ", context) : wxString()),
1596
0
            m_lang
1597
0
        );
1598
0
    }
1599
1600
0
    return trans;
1601
0
}
1602
1603
wxString wxTranslations::GetHeaderValue(const wxString& header,
1604
                                        const wxString& domain) const
1605
0
{
1606
0
    if ( header.empty() )
1607
0
        return wxEmptyString;
1608
1609
0
    const wxString *trans = nullptr;
1610
0
    wxMsgCatalog *pMsgCat;
1611
1612
0
    if ( !domain.empty() )
1613
0
    {
1614
0
        pMsgCat = FindCatalog(domain);
1615
1616
        // does the catalog exist?
1617
0
        if ( pMsgCat == nullptr )
1618
0
            return wxEmptyString;
1619
1620
0
        trans = pMsgCat->GetString(wxEmptyString, UINT_MAX);
1621
0
    }
1622
0
    else
1623
0
    {
1624
        // search in all domains
1625
0
        for ( pMsgCat = m_pMsgCat; pMsgCat != nullptr; pMsgCat = pMsgCat->m_pNext )
1626
0
        {
1627
0
            trans = pMsgCat->GetString(wxEmptyString, UINT_MAX);
1628
0
            if ( trans != nullptr )   // take the first found
1629
0
                break;
1630
0
        }
1631
0
    }
1632
1633
0
    if ( !trans || trans->empty() )
1634
0
        return wxEmptyString;
1635
1636
0
    size_t found = trans->find(header + wxS(": "));
1637
0
    if ( found == wxString::npos )
1638
0
        return wxEmptyString;
1639
1640
0
    found += header.length() + 2 /* ': ' */;
1641
1642
    // Every header is separated by \n
1643
1644
0
    size_t endLine = trans->find(wxS('\n'), found);
1645
0
    size_t len = (endLine == wxString::npos) ?
1646
0
                wxString::npos : (endLine - found);
1647
1648
0
    return trans->substr(found, len);
1649
0
}
1650
1651
1652
// find catalog by name
1653
wxMsgCatalog *wxTranslations::FindCatalog(const wxString& domain) const
1654
0
{
1655
0
    const wxMsgCatalogMap::const_iterator found = m_catalogMap.find(domain);
1656
1657
0
    return found == m_catalogMap.end() ? nullptr : found->second;
1658
0
}
1659
1660
// ----------------------------------------------------------------------------
1661
// wxFileTranslationsLoader
1662
// ----------------------------------------------------------------------------
1663
1664
namespace
1665
{
1666
1667
// the list of the directories to search for message catalog files
1668
wxArrayString gs_searchPrefixes;
1669
1670
// return the directories to search for message catalogs under the given
1671
// prefix, separated by wxPATH_SEP
1672
wxString GetMsgCatalogSubdirs(const wxString& prefix, const wxString& lang)
1673
0
{
1674
    // Search first in Unix-standard prefix/lang/LC_MESSAGES, then in
1675
    // prefix/lang.
1676
    //
1677
    // Note that we use LC_MESSAGES on all platforms and not just Unix, because
1678
    // it doesn't cost much to look into one more directory and doing it this
1679
    // way has two important benefits:
1680
    // a) we don't break compatibility with wx-2.6 and older by stopping to
1681
    //    look in a directory where the catalogs used to be and thus silently
1682
    //    breaking apps after they are recompiled against the latest wx
1683
    // b) it makes it possible to package app's support files in the same
1684
    //    way on all target platforms
1685
0
    const wxString prefixAndLang = wxFileName(prefix, lang).GetFullPath();
1686
1687
0
    wxString searchPath;
1688
0
    searchPath.reserve(4*prefixAndLang.length());
1689
1690
0
    searchPath
1691
#ifdef __WXOSX__
1692
               << prefixAndLang << ".lproj/LC_MESSAGES" << wxPATH_SEP
1693
               << prefixAndLang << ".lproj" << wxPATH_SEP
1694
#endif
1695
0
               << prefixAndLang << wxFILE_SEP_PATH << "LC_MESSAGES" << wxPATH_SEP
1696
0
               << prefixAndLang << wxPATH_SEP
1697
0
               ;
1698
1699
0
    return searchPath;
1700
0
}
1701
1702
bool HasMsgCatalogInDir(const wxString& dir, const wxString& domain)
1703
0
{
1704
0
    return wxFileName(dir, domain, "mo").FileExists() ||
1705
0
           wxFileName(dir + wxFILE_SEP_PATH + "LC_MESSAGES", domain, "mo").FileExists();
1706
0
}
1707
1708
// get prefixes to locale directories; if lang is empty, don't point to
1709
// OSX's .lproj bundles
1710
wxArrayString GetSearchPrefixes()
1711
0
{
1712
0
    wxArrayString paths;
1713
1714
    // first take the entries explicitly added by the program
1715
0
    paths = gs_searchPrefixes;
1716
1717
0
#if wxUSE_STDPATHS
1718
    // then look in the standard location
1719
0
    wxString stdp;
1720
0
    stdp = wxStandardPaths::Get().GetResourcesDir();
1721
0
    if ( paths.Index(stdp) == wxNOT_FOUND )
1722
0
        paths.Add(stdp);
1723
1724
0
  #ifdef wxHAS_STDPATHS_INSTALL_PREFIX
1725
0
    stdp = wxStandardPaths::Get().GetInstallPrefix() + "/share/locale";
1726
0
    if ( paths.Index(stdp) == wxNOT_FOUND )
1727
0
        paths.Add(stdp);
1728
0
  #endif
1729
0
#endif // wxUSE_STDPATHS
1730
1731
    // last look in default locations
1732
0
#ifdef __UNIX__
1733
    // LC_PATH is a standard env var containing the search path for the .mo
1734
    // files
1735
0
    const char *pszLcPath = wxGetenv("LC_PATH");
1736
0
    if ( pszLcPath )
1737
0
    {
1738
0
        const wxString lcp = pszLcPath;
1739
0
        if ( paths.Index(lcp) == wxNOT_FOUND )
1740
0
            paths.Add(lcp);
1741
0
    }
1742
1743
    // also add the one from where wxWin was installed:
1744
0
    wxString wxp = wxGetInstallPrefix();
1745
0
    if ( !wxp.empty() )
1746
0
    {
1747
0
        wxp += wxS("/share/locale");
1748
0
        if ( paths.Index(wxp) == wxNOT_FOUND )
1749
0
            paths.Add(wxp);
1750
0
    }
1751
0
#endif // __UNIX__
1752
1753
0
    return paths;
1754
0
}
1755
1756
// construct the search path for the given language
1757
wxString GetFullSearchPath(const wxString& lang)
1758
0
{
1759
0
    wxString searchPath;
1760
0
    searchPath.reserve(500);
1761
1762
0
    const wxArrayString prefixes = GetSearchPrefixes();
1763
1764
0
    for ( wxArrayString::const_iterator i = prefixes.begin();
1765
0
          i != prefixes.end();
1766
0
          ++i )
1767
0
    {
1768
0
        const wxString p = GetMsgCatalogSubdirs(*i, lang);
1769
1770
0
        if ( !searchPath.empty() )
1771
0
            searchPath += wxPATH_SEP;
1772
0
        searchPath += p;
1773
0
    }
1774
1775
0
    return searchPath;
1776
0
}
1777
1778
} // anonymous namespace
1779
1780
1781
void wxFileTranslationsLoader::AddCatalogLookupPathPrefix(const wxString& prefix)
1782
0
{
1783
0
    if ( gs_searchPrefixes.Index(prefix) == wxNOT_FOUND )
1784
0
    {
1785
0
        gs_searchPrefixes.Add(prefix);
1786
0
    }
1787
    //else: already have it
1788
0
}
1789
1790
1791
wxMsgCatalog *wxFileTranslationsLoader::LoadCatalog(const wxString& domain,
1792
                                                    const wxString& lang)
1793
0
{
1794
0
    wxString searchPath = GetFullSearchPath(lang);
1795
1796
0
    LogTraceLargeArray
1797
0
    (
1798
0
        wxString::Format("looking for \"%s.mo\" in search path", domain),
1799
0
        wxSplit(searchPath, wxPATH_SEP[0])
1800
0
    );
1801
1802
0
    wxFileName fn(wxString(), domain, wxS("mo"));
1803
1804
0
    wxString strFullName;
1805
0
    if ( !wxFindFileInPath(&strFullName, searchPath, fn.GetFullPath()) )
1806
0
        return nullptr;
1807
1808
    // open file and read its data
1809
0
    wxLogVerbose(_("using catalog '%s' from '%s'."), domain, strFullName);
1810
0
    wxLogTrace(TRACE_I18N, wxS("Using catalog \"%s\"."), strFullName);
1811
1812
0
    return wxMsgCatalog::CreateFromFile(strFullName, domain);
1813
0
}
1814
1815
1816
wxArrayString wxFileTranslationsLoader::GetAvailableTranslations(const wxString& domain) const
1817
0
{
1818
0
    wxArrayString langs;
1819
0
    const wxArrayString prefixes = GetSearchPrefixes();
1820
1821
0
    LogTraceLargeArray
1822
0
    (
1823
0
        wxString::Format("looking for available translations of \"%s\" in search path", domain),
1824
0
        prefixes
1825
0
    );
1826
1827
0
    for ( wxArrayString::const_iterator i = prefixes.begin();
1828
0
          i != prefixes.end();
1829
0
          ++i )
1830
0
    {
1831
0
        if ( i->empty() )
1832
0
            continue;
1833
0
        wxDir dir;
1834
0
        if ( !dir.Open(*i) )
1835
0
            continue;
1836
1837
0
        wxString lang;
1838
0
        for ( bool ok = dir.GetFirst(&lang, wxString(), wxDIR_DIRS);
1839
0
              ok;
1840
0
              ok = dir.GetNext(&lang) )
1841
0
        {
1842
0
            const wxString langdir = *i + wxFILE_SEP_PATH + lang;
1843
0
            if ( HasMsgCatalogInDir(langdir, domain) )
1844
0
            {
1845
#ifdef __WXOSX__
1846
                wxString rest;
1847
                if ( lang.EndsWith(".lproj", &rest) )
1848
                    lang = rest;
1849
#endif // __WXOSX__
1850
1851
0
                wxLogTrace(TRACE_I18N,
1852
0
                           "found %s translation of \"%s\" in %s",
1853
0
                           lang, domain, langdir);
1854
0
                langs.push_back(lang);
1855
0
            }
1856
0
        }
1857
0
    }
1858
1859
0
    return langs;
1860
0
}
1861
1862
1863
// ----------------------------------------------------------------------------
1864
// wxResourceTranslationsLoader
1865
// ----------------------------------------------------------------------------
1866
1867
#ifdef __WINDOWS__
1868
1869
wxMsgCatalog *wxResourceTranslationsLoader::LoadCatalog(const wxString& domain,
1870
                                                        const wxString& lang)
1871
{
1872
    const void *mo_data = nullptr;
1873
    size_t mo_size = 0;
1874
1875
    // Language may contain non-alphabetic characters that are not allowed in the
1876
    // resource names that must be valid identifiers, so sanitize the language
1877
    // before using it as part of the resource name.
1878
    wxString lang_sanitized = lang;
1879
    for ( wxString::iterator it = lang_sanitized.begin(); it != lang_sanitized.end(); ++it )
1880
    {
1881
        const wxUniChar c = *it;
1882
        if ( !((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) )
1883
            *it = '_';
1884
    }
1885
    const wxString resname = wxString::Format("%s_%s", domain, lang_sanitized);
1886
1887
    if ( !wxLoadUserResource(&mo_data, &mo_size,
1888
                             resname,
1889
                             GetResourceType().t_str(),
1890
                             GetModule()) )
1891
        return nullptr;
1892
1893
    wxLogTrace(TRACE_I18N,
1894
               "Using catalog from Windows resource \"%s\".", resname);
1895
1896
    wxMsgCatalog *cat = wxMsgCatalog::CreateFromData(
1897
        wxCharBuffer::CreateNonOwned(static_cast<const char*>(mo_data), mo_size),
1898
        domain);
1899
1900
    if ( !cat )
1901
    {
1902
        wxLogWarning(_("Resource '%s' is not a valid message catalog."), resname);
1903
    }
1904
1905
    return cat;
1906
}
1907
1908
namespace
1909
{
1910
1911
struct EnumCallbackData
1912
{
1913
    wxString prefix;
1914
    wxArrayString langs;
1915
};
1916
1917
BOOL CALLBACK EnumTranslations(HMODULE WXUNUSED(hModule),
1918
                               LPCTSTR WXUNUSED(lpszType),
1919
                               LPTSTR lpszName,
1920
                               LONG_PTR lParam)
1921
{
1922
    wxString name(lpszName);
1923
    name.MakeLower(); // resource names are case insensitive
1924
1925
    EnumCallbackData *data = reinterpret_cast<EnumCallbackData*>(lParam);
1926
1927
    wxString lang;
1928
    if ( name.StartsWith(data->prefix, &lang) && !lang.empty() )
1929
        data->langs.push_back(lang);
1930
1931
    return TRUE; // continue enumeration
1932
}
1933
1934
} // anonymous namespace
1935
1936
1937
wxArrayString wxResourceTranslationsLoader::GetAvailableTranslations(const wxString& domain) const
1938
{
1939
    EnumCallbackData data;
1940
    data.prefix = domain + "_";
1941
    data.prefix.MakeLower(); // resource names are case insensitive
1942
1943
    if ( !EnumResourceNames(GetModule(),
1944
                            GetResourceType().t_str(),
1945
                            EnumTranslations,
1946
                            reinterpret_cast<LONG_PTR>(&data)) )
1947
    {
1948
        const DWORD err = GetLastError();
1949
        if ( err != NO_ERROR && err != ERROR_RESOURCE_TYPE_NOT_FOUND )
1950
        {
1951
            wxLogSysError(_("Couldn't enumerate translations"));
1952
        }
1953
    }
1954
1955
    return data.langs;
1956
}
1957
1958
#endif // __WINDOWS__
1959
1960
1961
// ----------------------------------------------------------------------------
1962
// wxTranslationsModule module (for destruction of gs_translations)
1963
// ----------------------------------------------------------------------------
1964
1965
class wxTranslationsModule: public wxModule
1966
{
1967
    wxDECLARE_DYNAMIC_CLASS(wxTranslationsModule);
1968
public:
1969
0
        wxTranslationsModule() {}
1970
1971
        bool OnInit() override
1972
0
        {
1973
0
            return true;
1974
0
        }
1975
1976
        void OnExit() override
1977
0
        {
1978
0
            if ( gs_translationsOwned )
1979
0
                delete gs_translations;
1980
0
            gs_translations = nullptr;
1981
0
            gs_translationsOwned = true;
1982
0
        }
1983
};
1984
1985
wxIMPLEMENT_DYNAMIC_CLASS(wxTranslationsModule, wxModule);
1986
1987
#endif // wxUSE_INTL