| 1 | /* |
| 2 | i18n.c: which language the UI is in, and its words from data/lang/<code>.txt. |
| 3 |
|
| 4 | The PS3's own language unless one was picked in the app (USRDIR/language.txt: |
| 5 | a code, or "auto"). Arabic and Indonesian aren't PS3 system languages: they're |
| 6 | only there by picking them. (Russian, Chinese, Japanese and Korean are: they follow the PS3.) |
| 7 |
|
| 8 | A line of a language file is "English<TAB>translation"; "\n" in either is a |
| 9 | new line and a line starting with # is a comment. A translation whose printf |
| 10 | conversions (%s, %d...) aren't the English's, in the same order, is left out: |
| 11 | it would read the wrong arguments. |
| 12 | */ |
| 13 | #include <stdio.h> |
| 14 | #include <stdlib.h> |
| 15 | #include <string.h> |
| 16 | #include "plat.h" |
| 17 | #include "lang.h" |
| 18 | #include "i18n.h" |
| 19 |
|
| 20 | static int choice = -1; // -1: the PS3's language |
| 21 | static const lang_table_t *tables[LANG_COUNT]; // loaded once each, kept (tr() may be reading one) |
| 22 |
|
| 23 | // the conversions of a format, in order ("%s %d" -> "sd") |
| 24 | static void Specs(const char *s, char *out, int size) |
| 25 | { |
| 26 | int n = 0; |
| 27 | for (; *s && n < size - 1; s++) { |
| 28 | if (*s != '%') continue; |
| 29 | s++; |
| 30 | if (*s == '%') continue; |
| 31 | while (*s && strchr("-+ #0123456789.lhz", *s)) s++; |
| 32 | if (!*s) break; |
| 33 | out[n++] = *s; |
| 34 | } |
| 35 | out[n] = 0; |
| 36 | } |
| 37 |
|
| 38 | // "\n" -> a new line, "\\" -> "\", in place |
| 39 | static void Unescape(char *s) |
| 40 | { |
| 41 | char *d = s; |
| 42 | for (; *s; s++) { |
| 43 | if (*s == '\\' && s[1] == 'n') { *d++ = '\n'; s++; } |
| 44 | else if (*s == '\\' && s[1] == '\\') { *d++ = '\\'; s++; } |
| 45 | else *d++ = *s; |
| 46 | } |
| 47 | *d = 0; |
| 48 | } |
| 49 |
|
| 50 | static const lang_table_t *Load(int lang) |
| 51 | { |
| 52 | char path[512]; |
| 53 | snprintf(path, sizeof(path), "%s/lang/%s.txt", plat_data_dir(), lang_code(lang)); |
| 54 | char *text = (char *)plat_read_file(path, NULL); // (kept: the table's strings are in it) |
| 55 | if (!text) { |
| 56 | plat_log("lang: no %s: English", path); |
| 57 | return NULL; |
| 58 | } |
| 59 | int lines = 1; |
| 60 | for (const char *p = text; *p; p++) lines += *p == '\n'; |
| 61 | int size = 16; |
| 62 | while (size < lines * 2) size *= 2; |
| 63 | lang_table_t *t = calloc(1, sizeof(*t)); |
| 64 | const char **en = calloc(size, sizeof(char *)), **tx = calloc(size, sizeof(char *)); |
| 65 | if (!t || !en || !tx) { |
| 66 | free(t); free(en); free(tx); free(text); |
| 67 | return NULL; |
| 68 | } |
| 69 | t->size = size; |
| 70 | t->en = en; |
| 71 | t->tx = tx; |
| 72 | int n = 0, bad = 0; |
| 73 | for (char *line = text, *next; line && *line; line = next) { |
| 74 | next = strchr(line, '\n'); |
| 75 | if (next) *next++ = 0; |
| 76 | char *cr = strchr(line, '\r'); |
| 77 | if (cr) *cr = 0; |
| 78 | char *tab = strchr(line, '\t'); |
| 79 | if (line[0] == '#' || !tab || !tab[1]) continue; |
| 80 | *tab = 0; |
| 81 | char *e = line, *x = tab + 1; |
| 82 | Unescape(e); |
| 83 | Unescape(x); |
| 84 | char se[32], sx[32]; |
| 85 | Specs(e, se, sizeof(se)); |
| 86 | Specs(x, sx, sizeof(sx)); |
| 87 | if (strcmp(se, sx)) { |
| 88 | plat_log("lang: %s: \"%s\" left out (its %% conversions differ)", lang_code(lang), e); |
| 89 | bad++; |
| 90 | continue; |
| 91 | } |
| 92 | unsigned i = lang_hash(e) & (size - 1); |
| 93 | while (en[i] && strcmp(en[i], e)) i = (i + 1) & (size - 1); |
| 94 | en[i] = e; |
| 95 | tx[i] = x; |
| 96 | n++; |
| 97 | } |
| 98 | plat_log("lang: %s, %d phrases%s", lang_code(lang), n, bad ? " (some left out)" : ""); |
| 99 | return t; |
| 100 | } |
| 101 |
|
| 102 | // the PS3's language (sysutil's numbering), as one of ours |
| 103 | int i18n_system(void) |
| 104 | { |
| 105 | switch (plat_system_lang()) { |
| 106 | case 0: return LANG_JA; |
| 107 | case 2: return LANG_FR; |
| 108 | case 3: return LANG_ES; |
| 109 | case 4: return LANG_DE; |
| 110 | case 5: return LANG_IT; |
| 111 | case 6: return LANG_NL; |
| 112 | case 7: case 17: return LANG_PT; |
| 113 | case 8: return LANG_RU; |
| 114 | case 9: return LANG_KO; |
| 115 | case 10: case 11: return LANG_ZH; // (traditional too: simplified is closer than English) |
| 116 | case 14: return LANG_DA; |
| 117 | case 16: return LANG_PL; |
| 118 | case 19: return LANG_TR; |
| 119 | } |
| 120 | return LANG_EN; |
| 121 | } |
| 122 |
|
| 123 | static void Apply(void) |
| 124 | { |
| 125 | int l = choice >= 0 ? choice : i18n_system(); |
| 126 | if (l != LANG_EN && !tables[l]) tables[l] = Load(l); |
| 127 | lang_install(l, l == LANG_EN ? NULL : tables[l]); |
| 128 | } |
| 129 |
|
| 130 | void i18n_init(void) |
| 131 | { |
| 132 | char path[512]; |
| 133 | snprintf(path, sizeof(path), "%s/language.txt", plat_save_dir()); |
| 134 | char *s = (char *)plat_read_file(path, NULL); |
| 135 | if (s) { |
| 136 | for (int i = 0; i < LANG_COUNT; i++) |
| 137 | if (!strncmp(s, lang_code(i), 2) && (s[2] == 0 || s[2] == '\n' || s[2] == '\r')) choice = i; |
| 138 | free(s); |
| 139 | } |
| 140 | Apply(); |
| 141 | } |
| 142 |
|
| 143 | int i18n_choice(void) { return choice; } |
| 144 |
|
| 145 | void i18n_set(int lang) |
| 146 | { |
| 147 | char path[512]; |
| 148 | choice = lang >= 0 && lang < LANG_COUNT ? lang : -1; |
| 149 | snprintf(path, sizeof(path), "%s/language.txt", plat_save_dir()); |
| 150 | const char *v = choice >= 0 ? lang_code(choice) : "auto"; |
| 151 | plat_write_file(path, v, strlen(v)); |
| 152 | Apply(); |
| 153 | } |