Coverage Report

Created: 2025-12-05 06:49

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