Coverage Report

Created: 2026-10-17 07:24

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libcups/cups/language.c
Line
Count
Source
1
//
2
// I18N/language support for CUPS.
3
//
4
// Copyright © 2022-2026 by OpenPrinting.
5
// Copyright © 2007-2017 by Apple Inc.
6
// Copyright © 1997-2007 by Easy Software Products.
7
//
8
// Licensed under Apache License v2.0.  See the file "LICENSE" for more
9
// information.
10
//
11
12
#include "cups-private.h"
13
#include <sys/stat.h>
14
#if _WIN32
15
#  include <io.h>
16
#else
17
#  include <unistd.h>
18
#endif // _WIN32
19
20
#include "strings/ca_strings.h"
21
#include "strings/cs_strings.h"
22
#include "strings/da_strings.h"
23
#include "strings/de_strings.h"
24
#include "strings/en_strings.h"
25
#include "strings/es_strings.h"
26
#include "strings/fr_strings.h"
27
#include "strings/it_strings.h"
28
#include "strings/ja_strings.h"
29
#include "strings/pt_BR_strings.h"
30
#include "strings/ru_strings.h"
31
#include "strings/sv_strings.h"
32
#include "strings/zh_CN_strings.h"
33
34
35
//
36
// Types...
37
//
38
39
typedef struct _cups_message_s    // Message catalog entry
40
{
41
  char      *key,   // Key string
42
      *text;    // Localized text string
43
} _cups_message_t;
44
45
struct _cups_lang_s     // Language Cache
46
{
47
  cups_lang_t   *next;    // Next language in cache
48
  cups_rwlock_t   rwlock;   // Reader/writer lock
49
  char      language[16]; // Language/locale name
50
  size_t    num_messages, // Number of messages
51
      alloc_messages; // Allocated messages
52
  _cups_message_t *messages;  // Messages
53
};
54
55
56
//
57
// Local globals...
58
//
59
60
static cups_mutex_t lang_mutex = CUPS_MUTEX_INITIALIZER;
61
          // Mutex to control access to cache
62
static cups_lang_t  *lang_cache = NULL;
63
          // Language string cache
64
static char   *lang_directory = NULL;
65
          // Directory for strings files...
66
67
68
//
69
// Local functions...
70
//
71
72
static cups_lang_t  *cups_lang_new(const char *language);
73
static int    cups_message_compare(_cups_message_t *m1, _cups_message_t *m2);
74
75
76
//
77
// 'cupsLangAddStrings()' - Add strings for the specified language.
78
//
79
// This function adds strings for the specified language.  It is equivalent to
80
// calling @link cupsLangFind@ followed by @link cupsLangLoadStrings@.
81
//
82
83
bool          // O - `true` on success, `false` on failure
84
cupsLangAddStrings(
85
    const char *language,   // I - Language name
86
    const char *strings)    // I - Contents of ".strings" file
87
0
{
88
0
  cups_lang_t *lang;      // Language data
89
90
91
0
  if ((lang = cupsLangFind(language)) != NULL)
92
0
    return (cupsLangLoadStrings(lang, NULL, strings));
93
0
  else
94
0
    return (false);
95
0
}
96
97
98
//
99
// 'cupsLangFind()' - Find a language localization.
100
//
101
// This function finds the localization information for the specified language
102
// or locale name.
103
//
104
105
cups_lang_t *       // O - Language data
106
cupsLangFind(const char *language)  // I - Language or locale name, `NULL` for the current locale
107
0
{
108
0
  char    langname[16];   // Requested language name
109
0
  cups_lang_t *lang;      // Current language...
110
111
112
0
  DEBUG_printf("2cupsLangFind(language=\"%s\")", language);
113
114
0
  if (!language)
115
0
    return (cupsLangDefault());
116
117
0
  cupsMutexLock(&lang_mutex);
118
119
0
  cupsCopyString(langname, language, sizeof(langname));
120
0
  if (langname[2] == '-')
121
0
    langname[2] = '_';
122
123
0
  for (lang = lang_cache; lang; lang = lang->next)
124
0
  {
125
0
    if (!_cups_strcasecmp(lang->language, langname))
126
0
      break;
127
0
  }
128
129
0
  if (!lang)
130
0
  {
131
    // Create the language if it doesn't exist...
132
0
    lang = cups_lang_new(langname);
133
0
  }
134
135
0
  cupsMutexUnlock(&lang_mutex);
136
137
0
  return (lang);
138
0
}
139
140
141
//
142
// 'cupsLangFormatString()' - Create a localized formatted string.
143
//
144
// This function formats a string using the localized version of the "format"
145
// string argument, which supports all of the `printf` format specifiers.
146
//
147
148
const char *        // O - Formatted string
149
cupsLangFormatString(
150
    cups_lang_t *lang,      // I - Language data
151
    char        *buffer,    // I - Output buffer
152
    size_t      bufsize,    // I - Size of output buffer
153
    const char  *format,    // I - Printf-style format string
154
    ...)        // I - Additional arguments
155
0
{
156
0
  va_list ap;     // Pointer to additional arguments
157
158
159
0
  va_start(ap, format);
160
0
  vsnprintf(buffer, bufsize, cupsLangGetString(lang, format), ap);
161
0
  va_end(ap);
162
163
0
  return (buffer);
164
0
}
165
166
167
//
168
// 'cupsLangGetName()' - Get the language name.
169
//
170
171
const char *        // O - Language name
172
cupsLangGetName(cups_lang_t *lang)  // I - Language data
173
0
{
174
0
  return (lang ? lang->language : NULL);
175
0
}
176
177
178
//
179
// 'cupsLangGetString()' - Get a localized message string.
180
//
181
// This function gets a localized UTF-8 message string for the specified
182
// language.  If the message is not localized, the original message pointer is
183
// returned.
184
//
185
186
const char *        // O - Localized message
187
cupsLangGetString(cups_lang_t *lang,  // I - Language
188
                  const char  *message) // I - Message
189
0
{
190
0
  _cups_message_t key,    // Search key
191
0
      *match;   // Matching message
192
0
  const char    *text;    // Localized message text
193
194
195
0
  DEBUG_printf("cupsLangGetString(lang=%p(%s), message=\"%s\")", (void *)lang, lang ? lang->language : "null", message);
196
197
  // Range check input...
198
0
  if (!lang || !lang->num_messages || !message || !*message)
199
0
    return (message);
200
201
0
  cupsRWLockRead(&lang->rwlock);
202
203
0
  key.key = (char *)message;
204
0
  match   = bsearch(&key, lang->messages, lang->num_messages, sizeof(_cups_message_t), (int (*)(const void *, const void *))cups_message_compare);
205
0
  text    = match ? match->text : message;
206
207
0
  cupsRWUnlock(&lang->rwlock);
208
209
0
  return (text);
210
0
}
211
212
213
//
214
// 'cupsLangIsRTL()' - Get the writing direction.
215
//
216
// This function gets the writing direction for the specified language.
217
//
218
219
bool          // O - `true` if right-to-left, `false` if left-to-right
220
cupsLangIsRTL(cups_lang_t *lang)  // I - Language
221
0
{
222
  // Range check input...
223
0
  if (!lang)
224
0
    return (false);
225
226
  // The following languages are written from right to left: Arabic, Aramaic,
227
  // Azeri, Divehi, Fulah, Hebrew, Kurdish, N'ko, Persian, Rohingya, Syriac, and
228
  // Urdu.  Not all of these have language codes...
229
0
  return (!strncmp(lang->language, "ar", 2) || !strncmp(lang->language, "dv", 2) || !strncmp(lang->language, "ff", 2) || !strncmp(lang->language, "he", 2) || !strncmp(lang->language, "ku", 2) || !strncmp(lang->language, "fa", 2) || !strncmp(lang->language, "ur", 2));
230
0
}
231
232
233
//
234
// 'cupsLangLoadStrings()' - Load a message catalog for a language.
235
//
236
// This function loads a message catalog for a language.  The catalog must be in
237
// the Apple .strings format.  For example, the following translates the strings
238
// "Yes", "No", and "Welcome, %s." to French:
239
//
240
// ```
241
// "Yes" = "Oui";
242
// "No" = "Non";
243
// "Welcome, %s." = "Bievenue, %s.";
244
// ```
245
//
246
247
bool        // O - `true` on success, `false` on failure
248
cupsLangLoadStrings(
249
    cups_lang_t *lang,      // I - Language data
250
    const char  *filename,    // I - Filename or `NULL` for none
251
    const char  *strings)   // I - Strings or `NULL` for none
252
0
{
253
0
  bool    ret = true;   // Return value
254
0
  int   linenum;    // Current line number in data
255
0
  const char  *data,      // Pointer to strings data
256
0
    *dataptr;   // Pointer into string data
257
0
  char    key[1024],    // Key string
258
0
    text[1024],   // Localized text string
259
0
    *ptr;     // Pointer into strings
260
0
  _cups_message_t *m,     // Pointer to message
261
0
    mkey;     // Search key
262
0
  size_t  num_messages;   // New number of messages
263
264
265
0
  if (filename)
266
0
  {
267
    // Load the strings file...
268
0
    int   fd;     // File descriptor
269
0
    struct stat fileinfo;   // File information
270
0
    ssize_t bytes;      // Bytes read
271
272
0
    if ((fd = open(filename, O_RDONLY)) < 0)
273
0
    {
274
0
      _cupsSetError(IPP_STATUS_ERROR_INTERNAL, strerror(errno), 0);
275
0
      return (false);
276
0
    }
277
278
0
    if (fstat(fd, &fileinfo))
279
0
    {
280
0
      _cupsSetError(IPP_STATUS_ERROR_INTERNAL, strerror(errno), 0);
281
0
      close(fd);
282
0
      return (false);
283
0
    }
284
285
0
    if ((ptr = malloc((size_t)(fileinfo.st_size + 1))) == NULL)
286
0
    {
287
0
      _cupsSetError(IPP_STATUS_ERROR_INTERNAL, strerror(errno), 0);
288
0
      close(fd);
289
0
      return (false);
290
0
    }
291
292
0
    if ((bytes = read(fd, ptr, (size_t)fileinfo.st_size)) < 0)
293
0
    {
294
0
      _cupsSetError(IPP_STATUS_ERROR_INTERNAL, strerror(errno), 0);
295
0
      close(fd);
296
0
      free(ptr);
297
0
      return (false);
298
0
    }
299
300
0
    close(fd);
301
302
0
    ptr[bytes] = '\0';
303
0
    data       = ptr;
304
0
  }
305
0
  else
306
0
  {
307
    // Use in-memory strings data...
308
0
    data = (const char *)strings;
309
0
  }
310
311
0
  if (!data)
312
0
  {
313
0
    _cupsSetError(IPP_STATUS_ERROR_INTERNAL, strerror(EINVAL), 0);
314
0
    return (false);
315
0
  }
316
317
  // Scan the in-memory strings data and add key/text pairs...
318
  //
319
  // Format of strings files is:
320
  //
321
  // "key" = "text";
322
0
  cupsRWLockWrite(&lang->rwlock);
323
324
0
  num_messages = lang->num_messages;
325
0
  mkey.key     = key;
326
327
0
  for (dataptr = data, linenum = 1; *dataptr; dataptr ++)
328
0
  {
329
    // Skip leading whitespace...
330
0
    while (*dataptr && _cups_isspace(*dataptr))
331
0
    {
332
0
      if (*dataptr == '\n')
333
0
        linenum ++;
334
335
0
      dataptr ++;
336
0
    }
337
338
0
    if (!*dataptr)
339
0
    {
340
      // End of string...
341
0
      break;
342
0
    }
343
0
    else if (*dataptr == '/' && dataptr[1] == '*')
344
0
    {
345
      // Start of C-style comment...
346
0
      for (dataptr += 2; *dataptr; dataptr ++)
347
0
      {
348
0
        if (*dataptr == '*' && dataptr[1] == '/')
349
0
  {
350
0
    dataptr += 2;
351
0
    break;
352
0
  }
353
0
  else if (*dataptr == '\n')
354
0
    linenum ++;
355
0
      }
356
357
0
      if (!*dataptr)
358
0
        break;
359
0
    }
360
0
    else if (*dataptr != '\"')
361
0
    {
362
      // Something else we don't recognize...
363
0
      snprintf(text, sizeof(text), "Syntax error on line %d of '%s'.", linenum, filename ? filename : "in-memory");
364
0
      _cupsSetError(IPP_STATUS_ERROR_INTERNAL, text, 0);
365
0
      ret = false;
366
0
      break;
367
0
    }
368
369
    // Parse key string...
370
0
    dataptr ++;
371
0
    for (ptr = key; *dataptr && *dataptr != '\"'; dataptr ++)
372
0
    {
373
0
      if (*dataptr == '\\' && dataptr[1])
374
0
      {
375
        // Escaped character...
376
0
        int ch;     // Character
377
378
0
        dataptr ++;
379
0
        if (*dataptr == '\\' || *dataptr == '\'' || *dataptr == '\"')
380
0
        {
381
0
          ch = *dataptr;
382
0
  }
383
0
  else if (*dataptr == 'n')
384
0
  {
385
0
    ch = '\n';
386
0
  }
387
0
  else if (*dataptr == 'r')
388
0
  {
389
0
    ch = '\r';
390
0
  }
391
0
  else if (*dataptr == 't')
392
0
  {
393
0
    ch = '\t';
394
0
  }
395
0
  else if (*dataptr >= '0' && *dataptr <= '3' && dataptr[1] >= '0' && dataptr[1] <= '7' && dataptr[2] >= '0' && dataptr[2] <= '7')
396
0
  {
397
    // Octal escape
398
0
    ch = ((*dataptr - '0') << 6) | ((dataptr[1] - '0') << 3) | (dataptr[2] - '0');
399
0
    dataptr += 2;
400
0
  }
401
0
  else
402
0
  {
403
0
    snprintf(text, sizeof(text), "Invalid escape in key string on line %d of '%s'.", linenum, filename ? filename : "in-memory");
404
0
    _cupsSetError(IPP_STATUS_ERROR_INTERNAL, text, 0);
405
0
    ret = false;
406
0
    break;
407
0
  }
408
409
0
        if (ptr < (key + sizeof(key) - 1))
410
0
          *ptr++ = (char)ch;
411
0
      }
412
0
      else if (ptr < (key + sizeof(key) - 1))
413
0
      {
414
0
        *ptr++ = *dataptr;
415
0
      }
416
0
    }
417
418
0
    if (!*dataptr)
419
0
    {
420
0
      snprintf(text, sizeof(text), "Unterminated key string on line %d of '%s'.", linenum, filename ? filename : "in-memory");
421
0
      _cupsSetError(IPP_STATUS_ERROR_INTERNAL, text, 0);
422
0
      ret = false;
423
0
      break;
424
0
    }
425
426
0
    dataptr ++;
427
0
    *ptr = '\0';
428
429
    // Parse separator...
430
0
    while (*dataptr && _cups_isspace(*dataptr))
431
0
    {
432
0
      if (*dataptr == '\n')
433
0
        linenum ++;
434
435
0
      dataptr ++;
436
0
    }
437
438
0
    if (*dataptr != '=')
439
0
    {
440
0
      snprintf(text, sizeof(text), "Missing separator on line %d of '%s'.", linenum, filename ? filename : "in-memory");
441
0
      _cupsSetError(IPP_STATUS_ERROR_INTERNAL, text, 0);
442
0
      ret = false;
443
0
      break;
444
0
    }
445
446
0
    dataptr ++;
447
0
    while (*dataptr && _cups_isspace(*dataptr))
448
0
    {
449
0
      if (*dataptr == '\n')
450
0
        linenum ++;
451
452
0
      dataptr ++;
453
0
    }
454
455
0
    if (*dataptr != '\"')
456
0
    {
457
0
      snprintf(text, sizeof(text), "Missing text string on line %d of '%s'.", linenum, filename ? filename : "in-memory");
458
0
      _cupsSetError(IPP_STATUS_ERROR_INTERNAL, text, 0);
459
0
      ret = false;
460
0
      break;
461
0
    }
462
463
    // Parse text string...
464
0
    dataptr ++;
465
0
    for (ptr = text; *dataptr && *dataptr != '\"'; dataptr ++)
466
0
    {
467
0
      if (*dataptr == '\\')
468
0
      {
469
        // Escaped character...
470
0
        int ch;     // Character
471
472
0
        dataptr ++;
473
0
        if (*dataptr == '\\' || *dataptr == '\'' || *dataptr == '\"')
474
0
        {
475
0
          ch = *dataptr;
476
0
  }
477
0
  else if (*dataptr == 'n')
478
0
  {
479
0
    ch = '\n';
480
0
  }
481
0
  else if (*dataptr == 'r')
482
0
  {
483
0
    ch = '\r';
484
0
  }
485
0
  else if (*dataptr == 't')
486
0
  {
487
0
    ch = '\t';
488
0
  }
489
0
  else if (*dataptr >= '0' && *dataptr <= '3' && dataptr[1] >= '0' && dataptr[1] <= '7' && dataptr[2] >= '0' && dataptr[2] <= '7')
490
0
  {
491
    // Octal escape
492
0
    ch = ((*dataptr - '0') << 6) | ((dataptr[1] - '0') << 3) | (dataptr[2] - '0');
493
0
    dataptr += 2;
494
0
  }
495
0
  else
496
0
  {
497
0
    snprintf(text, sizeof(text), "Invalid escape in text string on line %d of '%s'.", linenum, filename ? filename : "in-memory");
498
0
    _cupsSetError(IPP_STATUS_ERROR_INTERNAL, text, 0);
499
0
    ret = false;
500
0
    break;
501
0
  }
502
503
0
        if (ptr < (text + sizeof(text) - 1))
504
0
          *ptr++ = (char)ch;
505
0
      }
506
0
      else if (ptr < (text + sizeof(text) - 1))
507
0
      {
508
0
        *ptr++ = *dataptr;
509
0
      }
510
0
    }
511
512
0
    if (!*dataptr)
513
0
    {
514
0
      snprintf(text, sizeof(text), "Unterminated text string on line %d of '%s'.", linenum, filename ? filename : "in-memory");
515
0
      _cupsSetError(IPP_STATUS_ERROR_INTERNAL, text, 0);
516
0
      ret = false;
517
0
      break;
518
0
    }
519
520
0
    dataptr ++;
521
0
    *ptr = '\0';
522
523
    // Look for terminator, then add the pair...
524
0
    if (*dataptr != ';')
525
0
    {
526
0
      snprintf(text, sizeof(text), "Missing terminator on line %d of '%s'.", linenum, filename ? filename : "in-memory");
527
0
      _cupsSetError(IPP_STATUS_ERROR_INTERNAL, text, 0);
528
0
      ret = false;
529
0
      break;
530
0
    }
531
532
    // Add the message if it doesn't already exist...
533
0
    if (lang->num_messages > 0 && bsearch(&mkey, lang->messages, lang->num_messages, sizeof(_cups_message_t), (int (*)(const void *, const void *))cups_message_compare))
534
0
      continue;
535
536
0
    if (num_messages >= lang->alloc_messages)
537
0
    {
538
0
      if ((m = realloc(lang->messages, (lang->alloc_messages + 1024) * sizeof(_cups_message_t))) == NULL)
539
0
      {
540
0
        _cupsSetError(IPP_STATUS_ERROR_INTERNAL, strerror(errno), 0);
541
0
        ret = false;
542
0
        break;
543
0
      }
544
545
0
      lang->messages       = m;
546
0
      lang->alloc_messages += 1024;
547
0
    }
548
549
0
    m = lang->messages + num_messages;
550
551
0
    if ((m->key = _cupsStrAlloc(key)) == NULL || (m->text = _cupsStrAlloc(text)) == NULL)
552
0
    {
553
0
      _cupsSetError(IPP_STATUS_ERROR_INTERNAL, strerror(errno), 0);
554
0
      _cupsStrFree(m->key);
555
0
      _cupsStrFree(m->text);
556
0
      ret = false;
557
0
      break;
558
0
    }
559
560
0
    num_messages ++;
561
0
  }
562
563
  // Re-sort messages as needed...
564
0
  if (num_messages > lang->num_messages)
565
0
  {
566
0
    lang->num_messages = num_messages;
567
0
    qsort(lang->messages, lang->num_messages, sizeof(_cups_message_t), (int (*)(const void *, const void *))cups_message_compare);
568
0
  }
569
570
0
  cupsRWUnlock(&lang->rwlock);
571
572
  // Free temporary storage and return...
573
0
  if (data != strings)
574
0
    free((void *)data);
575
576
0
  return (ret);
577
0
}
578
579
580
//
581
// 'cupsLangSetDirectory()' - Set the directory containing localizations.
582
//
583
// This function sets the directory where .strings files can be found.  The
584
// files are named "LOCALE.string" where "LOCALE" is the locale name, for
585
// example "fr" for generic French and "fr-CA" for Canadian French.
586
//
587
588
void
589
cupsLangSetDirectory(const char *d) // I - Directory name
590
0
{
591
0
  if (d)
592
0
  {
593
0
    cupsMutexLock(&lang_mutex);
594
595
0
    free(lang_directory);
596
0
    lang_directory = strdup(d);
597
598
0
    cupsMutexUnlock(&lang_mutex);
599
0
  }
600
0
}
601
602
603
//
604
// 'cups_lang_new()' - Create a new language.
605
//
606
607
static cups_lang_t *      // O - Language data
608
cups_lang_new(const char *language) // I - Language name
609
0
{
610
0
  cups_lang_t *lang;      // Language data
611
0
  char    filename[1024];   // Strings file...
612
0
  bool    status;     // Load status
613
614
615
0
  DEBUG_printf("cups_lang_new(language=\"%s\")", language);
616
617
  // Create an empty language data structure...
618
0
  if ((lang = calloc(1, sizeof(cups_lang_t))) == NULL)
619
0
    return (NULL);
620
621
0
  cupsRWInit(&lang->rwlock);
622
0
  cupsCopyString(lang->language, language, sizeof(lang->language));
623
624
  // Add strings...
625
0
  if (!_cups_strncasecmp(language, "ca", 2))
626
0
    status = cupsLangLoadStrings(lang, NULL, ca_strings);
627
0
  else if (!_cups_strncasecmp(language, "cs", 2))
628
0
    status = cupsLangLoadStrings(lang, NULL, cs_strings);
629
0
  else if (!_cups_strncasecmp(language, "da", 2))
630
0
    status = cupsLangLoadStrings(lang, NULL, da_strings);
631
0
  else if (!_cups_strncasecmp(language, "de", 2))
632
0
    status = cupsLangLoadStrings(lang, NULL, de_strings);
633
0
  else if (!_cups_strncasecmp(language, "es", 2))
634
0
    status = cupsLangLoadStrings(lang, NULL, es_strings);
635
0
  else if (!_cups_strncasecmp(language, "fr", 2))
636
0
    status = cupsLangLoadStrings(lang, NULL, fr_strings);
637
0
  else if (!_cups_strncasecmp(language, "it", 2))
638
0
    status = cupsLangLoadStrings(lang, NULL, it_strings);
639
0
  else if (!_cups_strncasecmp(language, "ja", 2))
640
0
    status = cupsLangLoadStrings(lang, NULL, ja_strings);
641
0
  else if (!_cups_strncasecmp(language, "pt", 2))
642
0
    status = cupsLangLoadStrings(lang, NULL, pt_BR_strings);
643
0
  else if (!_cups_strncasecmp(language, "ru", 2))
644
0
    status = cupsLangLoadStrings(lang, NULL, ru_strings);
645
0
  else if (!_cups_strncasecmp(language, "sv", 2))
646
0
    status = cupsLangLoadStrings(lang, NULL, sv_strings);
647
0
  else if (!_cups_strncasecmp(language, "zh", 2))
648
0
    status = cupsLangLoadStrings(lang, NULL, zh_CN_strings);
649
0
  else
650
0
    status = cupsLangLoadStrings(lang, NULL, en_strings);
651
652
0
  DEBUG_printf("2cups_lang_new: status=%s", status ? "true" : "false");
653
654
0
  if (status && lang_directory)
655
0
  {
656
0
    snprintf(filename, sizeof(filename), "%s/%s.strings", lang_directory, language);
657
0
    if (access(filename, 0) && language[2])
658
0
    {
659
0
      char  baselang[3];    // Base language name
660
661
0
      cupsCopyString(baselang, language, sizeof(baselang));
662
0
      snprintf(filename, sizeof(filename), "%s/%s.strings", lang_directory, baselang);
663
0
    }
664
665
0
    if (!access(filename, 0))
666
0
      status = cupsLangLoadStrings(lang, filename, NULL);
667
0
  }
668
669
0
  if (!status)
670
0
  {
671
    // Free memory if the load failed...
672
0
    size_t  i;      // Looping var
673
674
0
    for (i = 0; i < lang->num_messages; i ++)
675
0
    {
676
0
      _cupsStrFree(lang->messages[i].key);
677
0
      _cupsStrFree(lang->messages[i].text);
678
0
    }
679
680
0
    free(lang->messages);
681
0
    free(lang);
682
683
0
    return (NULL);
684
0
  }
685
686
  // Add this language to the front of the list...
687
0
  lang->next = lang_cache;
688
0
  lang_cache = lang;
689
690
0
  return (lang);
691
0
}
692
693
694
//
695
// 'cups_message_compare()' - Compare two messages.
696
//
697
698
static int        // O - Result of comparison
699
cups_message_compare(
700
    _cups_message_t *m1,    // I - First message
701
    _cups_message_t *m2)    // I - Second message
702
0
{
703
0
  return (strcmp(m1->key, m2->key));
704
0
}