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