1/*
2 * PPD localization routines for CUPS.
3 *
4 * Copyright 2007-2017 by Apple Inc.
5 * Copyright 1997-2007 by Easy Software Products, all rights reserved.
6 *
7 * These coded instructions, statements, and computer programs are the
8 * property of Apple Inc. and are protected by Federal copyright
9 * law.  Distribution and use rights are outlined in the file "LICENSE.txt"
10 * which should have been included with this file.  If this file is
11 * missing or damaged, see the license at "http://www.cups.org/".
12 *
13 * PostScript is a trademark of Adobe Systems, Inc.
14 *
15 * This code and any derivative of it may be used and distributed
16 * freely under the terms of the GNU General Public License when
17 * used with GNU Ghostscript or its derivatives.  Use of the code
18 * (or any derivative of it) with software other than GNU
19 * GhostScript (or its derivatives) is governed by the CUPS license
20 * agreement.
21 *
22 * This file is subject to the Apple OS-Developed Software exception.
23 */
24
25/*
26 * Include necessary headers.
27 */
28
29#include "cups-private.h"
30#include "ppd-private.h"
31
32
33/*
34 * Local functions...
35 */
36
37static cups_lang_t	*ppd_ll_CC(char *ll_CC, size_t ll_CC_size);
38
39
40/*
41 * 'ppdLocalize()' - Localize the PPD file to the current locale.
42 *
43 * All groups, options, and choices are localized, as are ICC profile
44 * descriptions, printer presets, and custom option parameters.  Each
45 * localized string uses the UTF-8 character encoding.
46 *
47 * @since CUPS 1.2/macOS 10.5@
48 */
49
50int					/* O - 0 on success, -1 on error */
51ppdLocalize(ppd_file_t *ppd)		/* I - PPD file */
52{
53  int		i, j, k;		/* Looping vars */
54  ppd_group_t	*group;			/* Current group */
55  ppd_option_t	*option;		/* Current option */
56  ppd_choice_t	*choice;		/* Current choice */
57  ppd_coption_t	*coption;		/* Current custom option */
58  ppd_cparam_t	*cparam;		/* Current custom parameter */
59  ppd_attr_t	*attr,			/* Current attribute */
60		*locattr;		/* Localized attribute */
61  char		ckeyword[PPD_MAX_NAME],	/* Custom keyword */
62		ll_CC[6];		/* Language + country locale */
63
64
65 /*
66  * Range check input...
67  */
68
69  DEBUG_printf(("ppdLocalize(ppd=%p)", ppd));
70
71  if (!ppd)
72    return (-1);
73
74 /*
75  * Get the default language...
76  */
77
78  ppd_ll_CC(ll_CC, sizeof(ll_CC));
79
80 /*
81  * Now lookup all of the groups, options, choices, etc.
82  */
83
84  for (i = ppd->num_groups, group = ppd->groups; i > 0; i --, group ++)
85  {
86    if ((locattr = _ppdLocalizedAttr(ppd, "Translation", group->name,
87                                     ll_CC)) != NULL)
88      strlcpy(group->text, locattr->text, sizeof(group->text));
89
90    for (j = group->num_options, option = group->options; j > 0; j --, option ++)
91    {
92      if ((locattr = _ppdLocalizedAttr(ppd, "Translation", option->keyword,
93                                       ll_CC)) != NULL)
94	strlcpy(option->text, locattr->text, sizeof(option->text));
95
96      for (k = option->num_choices, choice = option->choices;
97           k > 0;
98	   k --, choice ++)
99      {
100        if (strcmp(choice->choice, "Custom") ||
101	    !ppdFindCustomOption(ppd, option->keyword))
102	  locattr = _ppdLocalizedAttr(ppd, option->keyword, choice->choice,
103	                              ll_CC);
104	else
105	{
106	  snprintf(ckeyword, sizeof(ckeyword), "Custom%s", option->keyword);
107
108	  locattr = _ppdLocalizedAttr(ppd, ckeyword, "True", ll_CC);
109	}
110
111        if (locattr)
112	  strlcpy(choice->text, locattr->text, sizeof(choice->text));
113      }
114    }
115  }
116
117 /*
118  * Translate any custom parameters...
119  */
120
121  for (coption = (ppd_coption_t *)cupsArrayFirst(ppd->coptions);
122       coption;
123       coption = (ppd_coption_t *)cupsArrayNext(ppd->coptions))
124  {
125    for (cparam = (ppd_cparam_t *)cupsArrayFirst(coption->params);
126	 cparam;
127	 cparam = (ppd_cparam_t *)cupsArrayNext(coption->params))
128    {
129      snprintf(ckeyword, sizeof(ckeyword), "ParamCustom%s", coption->keyword);
130
131      if ((locattr = _ppdLocalizedAttr(ppd, ckeyword, cparam->name,
132                                       ll_CC)) != NULL)
133        strlcpy(cparam->text, locattr->text, sizeof(cparam->text));
134    }
135  }
136
137 /*
138  * Translate ICC profile names...
139  */
140
141  if ((attr = ppdFindAttr(ppd, "APCustomColorMatchingName", NULL)) != NULL)
142  {
143    if ((locattr = _ppdLocalizedAttr(ppd, "APCustomColorMatchingName",
144                                     attr->spec, ll_CC)) != NULL)
145      strlcpy(attr->text, locattr->text, sizeof(attr->text));
146  }
147
148  for (attr = ppdFindAttr(ppd, "cupsICCProfile", NULL);
149       attr;
150       attr = ppdFindNextAttr(ppd, "cupsICCProfile", NULL))
151  {
152    cupsArraySave(ppd->sorted_attrs);
153
154    if ((locattr = _ppdLocalizedAttr(ppd, "cupsICCProfile", attr->spec,
155                                     ll_CC)) != NULL)
156      strlcpy(attr->text, locattr->text, sizeof(attr->text));
157
158    cupsArrayRestore(ppd->sorted_attrs);
159  }
160
161 /*
162  * Translate printer presets...
163  */
164
165  for (attr = ppdFindAttr(ppd, "APPrinterPreset", NULL);
166       attr;
167       attr = ppdFindNextAttr(ppd, "APPrinterPreset", NULL))
168  {
169    cupsArraySave(ppd->sorted_attrs);
170
171    if ((locattr = _ppdLocalizedAttr(ppd, "APPrinterPreset", attr->spec,
172                                     ll_CC)) != NULL)
173      strlcpy(attr->text, locattr->text, sizeof(attr->text));
174
175    cupsArrayRestore(ppd->sorted_attrs);
176  }
177
178  return (0);
179}
180
181
182/*
183 * 'ppdLocalizeAttr()' - Localize an attribute.
184 *
185 * This function uses the current locale to find the localized attribute for
186 * the given main and option keywords.  If no localized version of the
187 * attribute exists for the current locale, the unlocalized version is returned.
188 */
189
190ppd_attr_t *				/* O - Localized attribute or @code NULL@ if none exists */
191ppdLocalizeAttr(ppd_file_t *ppd,	/* I - PPD file */
192		const char *keyword,	/* I - Main keyword */
193		const char *spec)	/* I - Option keyword or @code NULL@ for none */
194{
195  ppd_attr_t	*locattr;		/* Localized attribute */
196  char		ll_CC[6];		/* Language + country locale */
197
198
199 /*
200  * Get the default language...
201  */
202
203  ppd_ll_CC(ll_CC, sizeof(ll_CC));
204
205 /*
206  * Find the localized attribute...
207  */
208
209  if (spec)
210    locattr = _ppdLocalizedAttr(ppd, keyword, spec, ll_CC);
211  else
212    locattr = _ppdLocalizedAttr(ppd, "Translation", keyword, ll_CC);
213
214  if (!locattr)
215    locattr = ppdFindAttr(ppd, keyword, spec);
216
217  return (locattr);
218}
219
220
221/*
222 * 'ppdLocalizeIPPReason()' - Get the localized version of a cupsIPPReason
223 *                            attribute.
224 *
225 * This function uses the current locale to find the corresponding reason
226 * text or URI from the attribute value. If "scheme" is NULL or "text",
227 * the returned value contains human-readable (UTF-8) text from the translation
228 * string or attribute value. Otherwise the corresponding URI is returned.
229 *
230 * If no value of the requested scheme can be found, NULL is returned.
231 *
232 * @since CUPS 1.3/macOS 10.5@
233 */
234
235const char *				/* O - Value or NULL if not found */
236ppdLocalizeIPPReason(
237    ppd_file_t *ppd,			/* I - PPD file */
238    const char *reason,			/* I - IPP reason keyword to look up */
239    const char *scheme,			/* I - URI scheme or NULL for text */
240    char       *buffer,			/* I - Value buffer */
241    size_t     bufsize)			/* I - Size of value buffer */
242{
243  cups_lang_t	*lang;			/* Current language */
244  ppd_attr_t	*locattr;		/* Localized attribute */
245  char		ll_CC[6],		/* Language + country locale */
246		*bufptr,		/* Pointer into buffer */
247		*bufend,		/* Pointer to end of buffer */
248		*valptr;		/* Pointer into value */
249  int		ch;			/* Hex-encoded character */
250  size_t	schemelen;		/* Length of scheme name */
251
252
253 /*
254  * Range check input...
255  */
256
257  if (buffer)
258    *buffer = '\0';
259
260  if (!ppd || !reason || (scheme && !*scheme) ||
261      !buffer || bufsize < PPD_MAX_TEXT)
262    return (NULL);
263
264 /*
265  * Get the default language...
266  */
267
268  lang = ppd_ll_CC(ll_CC, sizeof(ll_CC));
269
270 /*
271  * Find the localized attribute...
272  */
273
274  if ((locattr = _ppdLocalizedAttr(ppd, "cupsIPPReason", reason,
275                                   ll_CC)) == NULL)
276    locattr = ppdFindAttr(ppd, "cupsIPPReason", reason);
277
278  if (!locattr)
279  {
280    if (lang && (!scheme || !strcmp(scheme, "text")))
281    {
282     /*
283      * Try to localize a standard printer-state-reason keyword...
284      */
285
286      const char *message = NULL;	/* Localized message */
287
288      if (!strncmp(reason, "media-needed", 12))
289	message = _("Load paper.");
290      else if (!strncmp(reason, "media-jam", 9))
291	message = _("Paper jam.");
292      else if (!strncmp(reason, "offline", 7) ||
293		       !strncmp(reason, "shutdown", 8))
294	message = _("The printer is not connected.");
295      else if (!strncmp(reason, "toner-low", 9))
296	message = _("The printer is low on toner.");
297      else if (!strncmp(reason, "toner-empty", 11))
298	message = _("The printer may be out of toner.");
299      else if (!strncmp(reason, "cover-open", 10))
300	message = _("The printer's cover is open.");
301      else if (!strncmp(reason, "interlock-open", 14))
302	message = _("The printer's interlock is open.");
303      else if (!strncmp(reason, "door-open", 9))
304	message = _("The printer's door is open.");
305      else if (!strncmp(reason, "input-tray-missing", 18))
306	message = _("Paper tray is missing.");
307      else if (!strncmp(reason, "media-low", 9))
308	message = _("Paper tray is almost empty.");
309      else if (!strncmp(reason, "media-empty", 11))
310	message = _("Paper tray is empty.");
311      else if (!strncmp(reason, "output-tray-missing", 19))
312	message = _("Output bin is missing.");
313      else if (!strncmp(reason, "output-area-almost-full", 23))
314	message = _("Output bin is almost full.");
315      else if (!strncmp(reason, "output-area-full", 16))
316	message = _("Output bin is full.");
317      else if (!strncmp(reason, "marker-supply-low", 17))
318	message = _("The printer is low on ink.");
319      else if (!strncmp(reason, "marker-supply-empty", 19))
320	message = _("The printer may be out of ink.");
321      else if (!strncmp(reason, "marker-waste-almost-full", 24))
322	message = _("The printer's waste bin is almost full.");
323      else if (!strncmp(reason, "marker-waste-full", 17))
324	message = _("The printer's waste bin is full.");
325      else if (!strncmp(reason, "fuser-over-temp", 15))
326	message = _("The fuser's temperature is high.");
327      else if (!strncmp(reason, "fuser-under-temp", 16))
328	message = _("The fuser's temperature is low.");
329      else if (!strncmp(reason, "opc-near-eol", 12))
330	message = _("The optical photoconductor will need to be replaced soon.");
331      else if (!strncmp(reason, "opc-life-over", 13))
332	message = _("The optical photoconductor needs to be replaced.");
333      else if (!strncmp(reason, "developer-low", 13))
334	message = _("The developer unit will need to be replaced soon.");
335      else if (!strncmp(reason, "developer-empty", 15))
336	message = _("The developer unit needs to be replaced.");
337
338      if (message)
339      {
340        strlcpy(buffer, _cupsLangString(lang, message), bufsize);
341	return (buffer);
342      }
343    }
344
345    return (NULL);
346  }
347
348 /*
349  * Now find the value we need...
350  */
351
352  bufend = buffer + bufsize - 1;
353
354  if (!scheme || !strcmp(scheme, "text"))
355  {
356   /*
357    * Copy a text value (either the translation text or text:... URIs from
358    * the value...
359    */
360
361    strlcpy(buffer, locattr->text, bufsize);
362
363    for (valptr = locattr->value, bufptr = buffer; *valptr && bufptr < bufend;)
364    {
365      if (!strncmp(valptr, "text:", 5))
366      {
367       /*
368        * Decode text: URI and add to the buffer...
369	*/
370
371	valptr += 5;
372
373        while (*valptr && !_cups_isspace(*valptr) && bufptr < bufend)
374	{
375	  if (*valptr == '%' && isxdigit(valptr[1] & 255) &&
376	      isxdigit(valptr[2] & 255))
377	  {
378	   /*
379	    * Pull a hex-encoded character from the URI...
380	    */
381
382            valptr ++;
383
384	    if (isdigit(*valptr & 255))
385	      ch = (*valptr - '0') << 4;
386	    else
387	      ch = (tolower(*valptr) - 'a' + 10) << 4;
388	    valptr ++;
389
390	    if (isdigit(*valptr & 255))
391	      *bufptr++ = (char)(ch | (*valptr - '0'));
392	    else
393	      *bufptr++ = (char)(ch | (tolower(*valptr) - 'a' + 10));
394	    valptr ++;
395	  }
396	  else if (*valptr == '+')
397	  {
398	    *bufptr++ = ' ';
399	    valptr ++;
400	  }
401	  else
402	    *bufptr++ = *valptr++;
403        }
404      }
405      else
406      {
407       /*
408        * Skip this URI...
409	*/
410
411        while (*valptr && !_cups_isspace(*valptr))
412          valptr++;
413      }
414
415     /*
416      * Skip whitespace...
417      */
418
419      while (_cups_isspace(*valptr))
420	valptr ++;
421    }
422
423    if (bufptr > buffer)
424      *bufptr = '\0';
425
426    return (buffer);
427  }
428  else
429  {
430   /*
431    * Copy a URI...
432    */
433
434    schemelen = strlen(scheme);
435    if (scheme[schemelen - 1] == ':')	/* Force scheme to be just the name */
436      schemelen --;
437
438    for (valptr = locattr->value, bufptr = buffer; *valptr && bufptr < bufend;)
439    {
440      if ((!strncmp(valptr, scheme, schemelen) && valptr[schemelen] == ':') ||
441          (*valptr == '/' && !strcmp(scheme, "file")))
442      {
443       /*
444        * Copy URI...
445	*/
446
447        while (*valptr && !_cups_isspace(*valptr) && bufptr < bufend)
448	  *bufptr++ = *valptr++;
449
450	*bufptr = '\0';
451
452	return (buffer);
453      }
454      else
455      {
456       /*
457        * Skip this URI...
458	*/
459
460	while (*valptr && !_cups_isspace(*valptr))
461	  valptr++;
462      }
463
464     /*
465      * Skip whitespace...
466      */
467
468      while (_cups_isspace(*valptr))
469	valptr ++;
470    }
471
472    return (NULL);
473  }
474}
475
476
477/*
478 * 'ppdLocalizeMarkerName()' - Get the localized version of a marker-names
479 *                             attribute value.
480 *
481 * This function uses the current locale to find the corresponding name
482 * text from the attribute value. If no localized text for the requested
483 * name can be found, @code NULL@ is returned.
484 *
485 * @since CUPS 1.4/macOS 10.6@
486 */
487
488const char *				/* O - Value or @code NULL@ if not found */
489ppdLocalizeMarkerName(
490    ppd_file_t *ppd,			/* I - PPD file */
491    const char *name)			/* I - Marker name to look up */
492{
493  ppd_attr_t	*locattr;		/* Localized attribute */
494  char		ll_CC[6];		/* Language + country locale */
495
496
497 /*
498  * Range check input...
499  */
500
501  if (!ppd || !name)
502    return (NULL);
503
504 /*
505  * Get the default language...
506  */
507
508  ppd_ll_CC(ll_CC, sizeof(ll_CC));
509
510 /*
511  * Find the localized attribute...
512  */
513
514  if ((locattr = _ppdLocalizedAttr(ppd, "cupsMarkerName", name,
515                                   ll_CC)) == NULL)
516    locattr = ppdFindAttr(ppd, "cupsMarkerName", name);
517
518  return (locattr ? locattr->text : NULL);
519}
520
521
522/*
523 * '_ppdFreeLanguages()' - Free an array of languages from _ppdGetLanguages.
524 */
525
526void
527_ppdFreeLanguages(
528    cups_array_t *languages)		/* I - Languages array */
529{
530  char	*language;			/* Current language */
531
532
533  for (language = (char *)cupsArrayFirst(languages);
534       language;
535       language = (char *)cupsArrayNext(languages))
536    free(language);
537
538  cupsArrayDelete(languages);
539}
540
541
542/*
543 * '_ppdGetLanguages()' - Get an array of languages from a PPD file.
544 */
545
546cups_array_t *				/* O - Languages array */
547_ppdGetLanguages(ppd_file_t *ppd)	/* I - PPD file */
548{
549  cups_array_t	*languages;		/* Languages array */
550  ppd_attr_t	*attr;			/* cupsLanguages attribute */
551  char		*value,			/* Copy of attribute value */
552		*start,			/* Start of current language */
553		*ptr;			/* Pointer into languages */
554
555
556 /*
557  * See if we have a cupsLanguages attribute...
558  */
559
560  if ((attr = ppdFindAttr(ppd, "cupsLanguages", NULL)) == NULL || !attr->value)
561    return (NULL);
562
563 /*
564  * Yes, load the list...
565  */
566
567  if ((languages = cupsArrayNew((cups_array_func_t)strcmp, NULL)) == NULL)
568    return (NULL);
569
570  if ((value = strdup(attr->value)) == NULL)
571  {
572    cupsArrayDelete(languages);
573    return (NULL);
574  }
575
576  for (ptr = value; *ptr;)
577  {
578   /*
579    * Skip leading whitespace...
580    */
581
582    while (_cups_isspace(*ptr))
583      ptr ++;
584
585    if (!*ptr)
586      break;
587
588   /*
589    * Find the end of this language name...
590    */
591
592    for (start = ptr; *ptr && !_cups_isspace(*ptr); ptr ++);
593
594    if (*ptr)
595      *ptr++ = '\0';
596
597    if (!strcmp(start, "en"))
598      continue;
599
600    cupsArrayAdd(languages, strdup(start));
601  }
602
603 /*
604  * Free the temporary string and return either an array with one or more
605  * values or a NULL pointer...
606  */
607
608  free(value);
609
610  if (cupsArrayCount(languages) == 0)
611  {
612    cupsArrayDelete(languages);
613    return (NULL);
614  }
615  else
616    return (languages);
617}
618
619
620/*
621 * '_ppdHashName()' - Generate a hash value for a device or profile name.
622 *
623 * This function is primarily used on macOS, but is generally accessible
624 * since cupstestppd needs to check for profile name collisions in PPD files...
625 */
626
627unsigned				/* O - Hash value */
628_ppdHashName(const char *name)		/* I - Name to hash */
629{
630  unsigned	mult,			/* Multiplier */
631		hash = 0;		/* Hash value */
632
633
634  for (mult = 1; *name && mult <= 128; mult ++, name ++)
635    hash += (*name & 255) * mult;
636
637  return (hash);
638}
639
640
641/*
642 * '_ppdLocalizedAttr()' - Find a localized attribute.
643 */
644
645ppd_attr_t *				/* O - Localized attribute or NULL */
646_ppdLocalizedAttr(ppd_file_t *ppd,	/* I - PPD file */
647		  const char *keyword,	/* I - Main keyword */
648		  const char *spec,	/* I - Option keyword */
649		  const char *ll_CC)	/* I - Language + country locale */
650{
651  char		lkeyword[PPD_MAX_NAME];	/* Localization keyword */
652  ppd_attr_t	*attr;			/* Current attribute */
653
654
655  DEBUG_printf(("4_ppdLocalizedAttr(ppd=%p, keyword=\"%s\", spec=\"%s\", "
656                "ll_CC=\"%s\")", ppd, keyword, spec, ll_CC));
657
658 /*
659  * Look for Keyword.ll_CC, then Keyword.ll...
660  */
661
662  snprintf(lkeyword, sizeof(lkeyword), "%s.%s", ll_CC, keyword);
663  if ((attr = ppdFindAttr(ppd, lkeyword, spec)) == NULL)
664  {
665   /*
666    * <rdar://problem/22130168>
667    *
668    * Multiple locales need special handling...  Sigh...
669    */
670
671    if (!strcmp(ll_CC, "zh_HK"))
672    {
673      snprintf(lkeyword, sizeof(lkeyword), "zh_TW.%s", keyword);
674      attr = ppdFindAttr(ppd, lkeyword, spec);
675    }
676
677    if (!attr)
678    {
679      snprintf(lkeyword, sizeof(lkeyword), "%2.2s.%s", ll_CC, keyword);
680      attr = ppdFindAttr(ppd, lkeyword, spec);
681    }
682
683    if (!attr)
684    {
685      if (!strncmp(ll_CC, "ja", 2))
686      {
687       /*
688	* Due to a bug in the CUPS DDK 1.1.0 ppdmerge program, Japanese
689	* PPD files were incorrectly assigned "jp" as the locale name
690	* instead of "ja".  Support both the old (incorrect) and new
691	* locale names for Japanese...
692	*/
693
694	snprintf(lkeyword, sizeof(lkeyword), "jp.%s", keyword);
695	attr = ppdFindAttr(ppd, lkeyword, spec);
696      }
697      else if (!strncmp(ll_CC, "nb", 2))
698      {
699       /*
700	* Norway has two languages, "Bokmal" (the primary one)
701	* and "Nynorsk" (new Norwegian); this code maps from the (currently)
702	* recommended "nb" to the previously recommended "no"...
703	*/
704
705	snprintf(lkeyword, sizeof(lkeyword), "no.%s", keyword);
706	attr = ppdFindAttr(ppd, lkeyword, spec);
707      }
708      else if (!strncmp(ll_CC, "no", 2))
709      {
710       /*
711	* Norway has two languages, "Bokmal" (the primary one)
712	* and "Nynorsk" (new Norwegian); we map "no" to "nb" here as
713	* recommended by the locale folks...
714	*/
715
716	snprintf(lkeyword, sizeof(lkeyword), "nb.%s", keyword);
717	attr = ppdFindAttr(ppd, lkeyword, spec);
718      }
719    }
720  }
721
722#ifdef DEBUG
723  if (attr)
724    DEBUG_printf(("5_ppdLocalizedAttr: *%s %s/%s: \"%s\"\n", attr->name,
725                  attr->spec, attr->text, attr->value ? attr->value : ""));
726  else
727    DEBUG_puts("5_ppdLocalizedAttr: NOT FOUND");
728#endif /* DEBUG */
729
730  return (attr);
731}
732
733
734/*
735 * 'ppd_ll_CC()' - Get the current locale names.
736 */
737
738static cups_lang_t *			/* O - Current language */
739ppd_ll_CC(char   *ll_CC,		/* O - Country-specific locale name */
740          size_t ll_CC_size)		/* I - Size of country-specific name */
741{
742  cups_lang_t	*lang;			/* Current language */
743
744
745 /*
746  * Get the current locale...
747  */
748
749  if ((lang = cupsLangDefault()) == NULL)
750  {
751    strlcpy(ll_CC, "en_US", ll_CC_size);
752    return (NULL);
753  }
754
755 /*
756  * Copy the locale name...
757  */
758
759  strlcpy(ll_CC, lang->language, ll_CC_size);
760
761  if (strlen(ll_CC) == 2)
762  {
763   /*
764    * Map "ll" to primary/origin country locales to have the best
765    * chance of finding a match...
766    */
767
768    if (!strcmp(ll_CC, "cs"))
769      strlcpy(ll_CC, "cs_CZ", ll_CC_size);
770    else if (!strcmp(ll_CC, "en"))
771      strlcpy(ll_CC, "en_US", ll_CC_size);
772    else if (!strcmp(ll_CC, "ja"))
773      strlcpy(ll_CC, "ja_JP", ll_CC_size);
774    else if (!strcmp(ll_CC, "sv"))
775      strlcpy(ll_CC, "sv_SE", ll_CC_size);
776    else if (!strcmp(ll_CC, "zh"))	/* Simplified Chinese */
777      strlcpy(ll_CC, "zh_CN", ll_CC_size);
778  }
779
780  DEBUG_printf(("8ppd_ll_CC: lang->language=\"%s\", ll_CC=\"%s\"...",
781                lang->language, ll_CC));
782  return (lang);
783}
784