Home | History | Annotate | Download | only in cups
      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 
     37 static 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 
     50 int					/* O - 0 on success, -1 on error */
     51 ppdLocalize(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 
    190 ppd_attr_t *				/* O - Localized attribute or @code NULL@ if none exists */
    191 ppdLocalizeAttr(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 
    235 const char *				/* O - Value or NULL if not found */
    236 ppdLocalizeIPPReason(
    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 
    488 const char *				/* O - Value or @code NULL@ if not found */
    489 ppdLocalizeMarkerName(
    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 
    526 void
    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 
    546 cups_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 
    627 unsigned				/* 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 
    645 ppd_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 
    738 static cups_lang_t *			/* O - Current language */
    739 ppd_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