1 | // SPDX-License-Identifier: MIT OR MPL-2.0 OR LGPL-2.1-or-later OR GPL-2.0-or-later |
2 | // Copyright 2010, SIL International, All rights reserved. |
3 | |
4 | #pragma once |
5 | #include <cstring> |
6 | #include <cassert> |
7 | |
8 | #include "inc/Main.h" |
9 | |
10 | |
11 | namespace graphite2 { |
12 | |
13 | struct IsoLangEntry |
14 | { |
15 | unsigned short mnLang; |
16 | char maLangStr[4]; |
17 | char maCountry[3]; |
18 | }; |
19 | |
20 | // Windows Language ID, Locale ISO-639 language, country code as used in |
21 | // naming table of OpenType fonts |
22 | const IsoLangEntry LANG_ENTRIES[] = { |
23 | { 0x0401, "ar" ,"SA" }, // Arabic Saudi Arabia |
24 | { 0x0402, "bg" ,"BG" }, // Bulgarian Bulgaria |
25 | { 0x0403, "ca" ,"ES" }, // Catalan Catalan |
26 | { 0x0404, "zh" ,"TW" }, // Chinese Taiwan |
27 | { 0x0405, "cs" ,"CZ" }, // Czech Czech Republic |
28 | { 0x0406, "da" ,"DK" }, // Danish Denmark |
29 | { 0x0407, "de" ,"DE" }, // German Germany |
30 | { 0x0408, "el" ,"GR" }, // Greek Greece |
31 | { 0x0409, "en" ,"US" }, // English United States |
32 | { 0x040A, "es" ,"ES" }, // Spanish (Traditional Sort) Spain |
33 | { 0x040B, "fi" ,"FI" }, // Finnish Finland |
34 | { 0x040C, "fr" ,"FR" }, // French France |
35 | { 0x040D, "he" ,"IL" }, // Hebrew Israel |
36 | { 0x040E, "hu" ,"HU" }, // Hungarian Hungary |
37 | { 0x040F, "is" ,"IS" }, // Icelandic Iceland |
38 | { 0x0410, "it" ,"IT" }, // Italian Italy |
39 | { 0x0411, "jp" ,"JP" }, // Japanese Japan |
40 | { 0x0412, "ko" ,"KR" }, // Korean Korea |
41 | { 0x0413, "nl" ,"NL" }, // Dutch Netherlands |
42 | { 0x0414, "no" ,"NO" }, // Norwegian (Bokmal) Norway |
43 | { 0x0415, "pl" ,"PL" }, // Polish Poland |
44 | { 0x0416, "pt" ,"BR" }, // Portuguese Brazil |
45 | { 0x0417, "rm" ,"CH" }, // Romansh Switzerland |
46 | { 0x0418, "ro" ,"RO" }, // Romanian Romania |
47 | { 0x0419, "ru" ,"RU" }, // Russian Russia |
48 | { 0x041A, "hr" ,"HR" }, // Croatian Croatia |
49 | { 0x041B, "sk" ,"SK" }, // Slovak Slovakia |
50 | { 0x041C, "sq" ,"AL" }, // Albanian Albania |
51 | { 0x041D, "sv" ,"SE" }, // Swedish Sweden |
52 | { 0x041E, "th" ,"TH" }, // Thai Thailand |
53 | { 0x041F, "tr" ,"TR" }, // Turkish Turkey |
54 | { 0x0420, "ur" ,"PK" }, // Urdu Islamic Republic of Pakistan |
55 | { 0x0421, "id" ,"ID" }, // Indonesian Indonesia |
56 | { 0x0422, "uk" ,"UA" }, // Ukrainian Ukraine |
57 | { 0x0423, "be" ,"BY" }, // Belarusian Belarus |
58 | { 0x0424, "sl" ,"SI" }, // Slovenian Slovenia |
59 | { 0x0425, "et" ,"EE" }, // Estonian Estonia |
60 | { 0x0426, "lv" ,"LV" }, // Latvian Latvia |
61 | { 0x0427, "lt" ,"LT" }, // Lithuanian Lithuania |
62 | { 0x0428, "tg" ,"TJ" }, // Tajik (Cyrillic) Tajikistan |
63 | { 0x042A, "vi" ,"VN" }, // Vietnamese Vietnam |
64 | { 0x042B, "hy" ,"AM" }, // Armenian Armenia |
65 | { 0x042C, "az" ,"AZ" }, // Azeri (Latin) Azerbaijan |
66 | { 0x042D, "eu" ,"" }, // Basque Basque |
67 | { 0x042E, "hsb" ,"DE" }, // Upper Sorbian Germany |
68 | { 0x042F, "mk" ,"MK" }, // Macedonian (FYROM) Former Yugoslav Republic of Macedonia |
69 | { 0x0432, "tn" ,"ZA" }, // Setswana South Africa |
70 | { 0x0434, "xh" ,"ZA" }, // isiXhosa South Africa |
71 | { 0x0435, "zu" ,"ZA" }, // isiZulu South Africa |
72 | { 0x0436, "af" ,"ZA" }, // Afrikaans South Africa |
73 | { 0x0437, "ka" ,"GE" }, // Georgian Georgia |
74 | { 0x0438, "fo" ,"FO" }, // Faroese Faroe Islands |
75 | { 0x0439, "hi" ,"IN" }, // Hindi India |
76 | { 0x043A, "mt" ,"MT" }, // Maltese Malta |
77 | { 0x043B, "se" ,"NO" }, // Sami (Northern) Norway |
78 | { 0x043E, "ms" ,"MY" }, // Malay Malaysia |
79 | { 0x043F, "kk" ,"KZ" }, // Kazakh Kazakhstan |
80 | { 0x0440, "ky" ,"KG" }, // Kyrgyz Kyrgyzstan |
81 | { 0x0441, "sw" ,"KE" }, // Kiswahili Kenya |
82 | { 0x0442, "tk" ,"TM" }, // Turkmen Turkmenistan |
83 | { 0x0443, "uz" ,"UZ" }, // Uzbek (Latin) Uzbekistan |
84 | { 0x0444, "tt" ,"RU" }, // Tatar Russia |
85 | { 0x0445, "bn" ,"IN" }, // Bengali India |
86 | { 0x0446, "pa" ,"IN" }, // Punjabi India |
87 | { 0x0447, "gu" ,"IN" }, // Gujarati India |
88 | { 0x0448, "or" ,"IN" }, // Oriya India |
89 | { 0x0448, "wo" ,"SN" }, // Wolof Senegal |
90 | { 0x0449, "ta" ,"IN" }, // Tamil India |
91 | { 0x044A, "te" ,"IN" }, // Telugu India |
92 | { 0x044B, "kn" ,"IN" }, // Kannada India |
93 | { 0x044C, "ml" ,"IN" }, // Malayalam India |
94 | { 0x044D, "as" ,"IN" }, // Assamese India |
95 | { 0x044E, "mr" ,"IN" }, // Marathi India |
96 | { 0x044F, "sa" ,"IN" }, // Sanskrit India |
97 | { 0x0450, "mn" ,"MN" }, // Mongolian (Cyrillic) Mongolia |
98 | { 0x0451, "bo" ,"CN" }, // Tibetan PRC |
99 | { 0x0452, "cy" ,"GB" }, // Welsh United Kingdom |
100 | { 0x0453, "km" ,"KH" }, // Khmer Cambodia |
101 | { 0x0454, "lo" ,"LA" }, // Lao Lao P.D.R. |
102 | { 0x0455, "my" ,"MM" }, // Burmese Myanmar - not listed in Microsoft docs anymore |
103 | { 0x0456, "gl" ,"ES" }, // Galician Galician |
104 | { 0x0457, "kok" ,"IN" }, // Konkani India |
105 | { 0x045A, "syr" ,"TR" }, // Syriac Syria |
106 | { 0x045B, "si" ,"LK" }, // Sinhala Sri Lanka |
107 | { 0x045D, "iu" ,"CA" }, // Inuktitut Canada |
108 | { 0x045E, "am" ,"ET" }, // Amharic Ethiopia |
109 | { 0x0461, "ne" ,"NP" }, // Nepali Nepal |
110 | { 0x0462, "fy" ,"NL" }, // Frisian Netherlands |
111 | { 0x0463, "ps" ,"AF" }, // Pashto Afghanistan |
112 | { 0x0464, "fil" ,"PH" }, // Filipino Philippines |
113 | { 0x0465, "dv" ,"MV" }, // Divehi Maldives |
114 | { 0x0468, "ha" ,"NG" }, // Hausa (Latin) Nigeria |
115 | { 0x046A, "yo" ,"NG" }, // Yoruba Nigeria |
116 | { 0x046B, "qu" ,"BO" }, // Quechua Bolivia |
117 | { 0x046C, "st" ,"ZA" }, // Sesotho sa Leboa South Africa |
118 | { 0x046D, "ba" ,"RU" }, // Bashkir Russia |
119 | { 0x046E, "lb" ,"LU" }, // Luxembourgish Luxembourg |
120 | { 0x046F, "kl" ,"GL" }, // Greenlandic Greenland |
121 | { 0x0470, "ig" ,"NG" }, // Igbo Nigeria |
122 | { 0x0478, "ii" ,"CN" }, // Yi PRC |
123 | { 0x047A, "arn" ,"CL" }, // Mapudungun Chile |
124 | { 0x047C, "moh" ,"CA" }, // Mohawk Mohawk |
125 | { 0x047E, "br" ,"FR" }, // Breton France |
126 | { 0x0480, "ug" ,"CN" }, // Uighur PRC |
127 | { 0x0481, "mi" ,"NZ" }, // Maori New Zealand |
128 | { 0x0482, "oc" ,"FR" }, // Occitan France |
129 | { 0x0483, "co" ,"FR" }, // Corsican France |
130 | { 0x0484, "gsw" ,"FR" }, // Alsatian France |
131 | { 0x0485, "sah" ,"RU" }, // Yakut Russia |
132 | { 0x0486, "qut" ,"GT" }, // K'iche Guatemala |
133 | { 0x0487, "rw" ,"RW" }, // Kinyarwanda Rwanda |
134 | { 0x048C, "gbz" ,"AF" }, // Dari Afghanistan |
135 | { 0x0801, "ar" ,"IQ" }, // Arabic Iraq |
136 | { 0x0804, "zn" ,"CH" }, // Chinese People's Republic of China |
137 | { 0x0807, "de" ,"CH" }, // German Switzerland |
138 | { 0x0809, "en" ,"GB" }, // English United Kingdom |
139 | { 0x080A, "es" ,"MX" }, // Spanish Mexico |
140 | { 0x080C, "fr" ,"BE" }, // French Belgium |
141 | { 0x0810, "it" ,"CH" }, // Italian Switzerland |
142 | { 0x0813, "nl" ,"BE" }, // Dutch Belgium |
143 | { 0x0814, "nn" ,"NO" }, // Norwegian (Nynorsk) Norway |
144 | { 0x0816, "pt" ,"PT" }, // Portuguese Portugal |
145 | { 0x081A, "sh" ,"RS" }, // Serbian (Latin) Serbia |
146 | { 0x081D, "sv" ,"FI" }, // Sweden Finland |
147 | { 0x082C, "az" ,"AZ" }, // Azeri (Cyrillic) Azerbaijan |
148 | { 0x082E, "dsb" ,"DE" }, // Lower Sorbian Germany |
149 | { 0x083B, "se" ,"SE" }, // Sami (Northern) Sweden |
150 | { 0x083C, "ga" ,"IE" }, // Irish Ireland |
151 | { 0x083E, "ms" ,"BN" }, // Malay Brunei Darussalam |
152 | { 0x0843, "uz" ,"UZ" }, // Uzbek (Cyrillic) Uzbekistan |
153 | { 0x0845, "bn" ,"BD" }, // Bengali Bangladesh |
154 | { 0x0850, "mn" ,"MN" }, // Mongolian (Traditional) People's Republic of China |
155 | { 0x085D, "iu" ,"CA" }, // Inuktitut (Latin) Canada |
156 | { 0x085F, "ber" ,"DZ" }, // Tamazight (Latin) Algeria |
157 | { 0x086B, "es" ,"EC" }, // Quechua Ecuador |
158 | { 0x0C01, "ar" ,"EG" }, // Arabic Egypt |
159 | { 0x0C04, "zh" ,"HK" }, // Chinese Hong Kong S.A.R. |
160 | { 0x0C07, "de" ,"AT" }, // German Austria |
161 | { 0x0C09, "en" ,"AU" }, // English Australia |
162 | { 0x0C0A, "es" ,"ES" }, // Spanish (Modern Sort) Spain |
163 | { 0x0C0C, "fr" ,"CA" }, // French Canada |
164 | { 0x0C1A, "sr" ,"CS" }, // Serbian (Cyrillic) Serbia |
165 | { 0x0C3B, "se" ,"FI" }, // Sami (Northern) Finland |
166 | { 0x0C6B, "qu" ,"PE" }, // Quechua Peru |
167 | { 0x1001, "ar" ,"LY" }, // Arabic Libya |
168 | { 0x1004, "zh" ,"SG" }, // Chinese Singapore |
169 | { 0x1007, "de" ,"LU" }, // German Luxembourg |
170 | { 0x1009, "en" ,"CA" }, // English Canada |
171 | { 0x100A, "es" ,"GT" }, // Spanish Guatemala |
172 | { 0x100C, "fr" ,"CH" }, // French Switzerland |
173 | { 0x101A, "hr" ,"BA" }, // Croatian (Latin) Bosnia and Herzegovina |
174 | { 0x103B, "smj" ,"NO" }, // Sami (Lule) Norway |
175 | { 0x1401, "ar" ,"DZ" }, // Arabic Algeria |
176 | { 0x1404, "zh" ,"MO" }, // Chinese Macao S.A.R. |
177 | { 0x1407, "de" ,"LI" }, // German Liechtenstein |
178 | { 0x1409, "en" ,"NZ" }, // English New Zealand |
179 | { 0x140A, "es" ,"CR" }, // Spanish Costa Rica |
180 | { 0x140C, "fr" ,"LU" }, // French Luxembourg |
181 | { 0x141A, "bs" ,"BA" }, // Bosnian (Latin) Bosnia and Herzegovina |
182 | { 0x143B, "smj" ,"SE" }, // Sami (Lule) Sweden |
183 | { 0x1801, "ar" ,"MA" }, // Arabic Morocco |
184 | { 0x1809, "en" ,"IE" }, // English Ireland |
185 | { 0x180A, "es" ,"PA" }, // Spanish Panama |
186 | { 0x180C, "fr" ,"MC" }, // French Principality of Monoco |
187 | { 0x181A, "sh" ,"BA" }, // Serbian (Latin) Bosnia and Herzegovina |
188 | { 0x183B, "sma" ,"NO" }, // Sami (Southern) Norway |
189 | { 0x1C01, "ar" ,"TN" }, // Arabic Tunisia |
190 | { 0x1C09, "en" ,"ZA" }, // English South Africa |
191 | { 0x1C0A, "es" ,"DO" }, // Spanish Dominican Republic |
192 | { 0x1C1A, "sr" ,"BA" }, // Serbian (Cyrillic) Bosnia and Herzegovina |
193 | { 0x1C3B, "sma" ,"SE" }, // Sami (Southern) Sweden |
194 | { 0x2001, "ar" ,"OM" }, // Arabic Oman |
195 | { 0x2009, "en" ,"JM" }, // English Jamaica |
196 | { 0x200A, "es" ,"VE" }, // Spanish Venezuela |
197 | { 0x201A, "bs" ,"BA" }, // Bosnian (Cyrillic) Bosnia and Herzegovina |
198 | { 0x203B, "sms" ,"FI" }, // Sami (Skolt) Finland |
199 | { 0x2401, "ar" ,"YE" }, // Arabic Yemen |
200 | { 0x2409, "en" ,"BS" }, // English Caribbean |
201 | { 0x240A, "es" ,"CO" }, // Spanish Colombia |
202 | { 0x243B, "smn" ,"FI" }, // Sami (Inari) Finland |
203 | { 0x2801, "ar" ,"SY" }, // Arabic Syria |
204 | { 0x2809, "en" ,"BZ" }, // English Belize |
205 | { 0x280A, "es" ,"PE" }, // Spanish Peru |
206 | { 0x2C01, "ar" ,"JO" }, // Arabic Jordan |
207 | { 0x2C09, "en" ,"TT" }, // English Trinidad and Tobago |
208 | { 0x2C0A, "es" ,"AR" }, // Spanish Argentina |
209 | { 0x3001, "ar" ,"LB" }, // Arabic Lebanon |
210 | { 0x3009, "en" ,"ZW" }, // English Zimbabwe |
211 | { 0x300A, "es" ,"EC" }, // Spanish Ecuador |
212 | { 0x3401, "ar" ,"KW" }, // Arabic Kuwait |
213 | { 0x3409, "en" ,"PH" }, // English Republic of the Philippines |
214 | { 0x340A, "es" ,"CL" }, // Spanish Chile |
215 | { 0x3801, "ar" ,"AE" }, // Arabic U.A.E. |
216 | { 0x380A, "es" ,"UY" }, // Spanish Uruguay |
217 | { 0x3C01, "ar" ,"BH" }, // Arabic Bahrain |
218 | { 0x3C0A, "es" ,"PY" }, // Spanish Paraguay |
219 | { 0x4001, "ar" ,"QA" }, // Arabic Qatar |
220 | { 0x4009, "en" ,"IN" }, // English India |
221 | { 0x400A, "es" ,"BO" }, // Spanish Bolivia |
222 | { 0x4409, "en" ,"MY" }, // English Malaysia |
223 | { 0x440A, "es" ,"SV" }, // Spanish El Salvador |
224 | { 0x4809, "en" ,"SG" }, // English Singapore |
225 | { 0x480A, "es" ,"HN" }, // Spanish Honduras |
226 | { 0x4C0A, "es" ,"NI" }, // Spanish Nicaragua |
227 | { 0x500A, "es" ,"PR" }, // Spanish Puerto Rico |
228 | { 0x540A, "es" ,"US" } // Spanish United States |
229 | }; |
230 | |
231 | class Locale2Lang |
232 | { |
233 | Locale2Lang(const Locale2Lang &); |
234 | Locale2Lang & operator = (const Locale2Lang &); |
235 | |
236 | public: |
237 | Locale2Lang() : mSeedPosition(128) |
238 | { |
239 | memset((void*)mLangLookup, 0, sizeof(mLangLookup)); |
240 | // create a tri lookup on first 2 letters of language code |
241 | static const int maxIndex = sizeof(LANG_ENTRIES)/sizeof(IsoLangEntry); |
242 | for (int i = 0; i < maxIndex; i++) |
243 | { |
244 | size_t a = LANG_ENTRIES[i].maLangStr[0] - 'a'; |
245 | size_t b = LANG_ENTRIES[i].maLangStr[1] - 'a'; |
246 | if (mLangLookup[a][b]) |
247 | { |
248 | const IsoLangEntry ** old = mLangLookup[a][b]; |
249 | int len = 1; |
250 | while (old[len]) len++; |
251 | len += 2; |
252 | mLangLookup[a][b] = gralloc<const IsoLangEntry *>(len); |
253 | if (!mLangLookup[a][b]) |
254 | { |
255 | mLangLookup[a][b] = old; |
256 | continue; |
257 | } |
258 | mLangLookup[a][b][--len] = NULL; |
259 | mLangLookup[a][b][--len] = &LANG_ENTRIES[i]; |
260 | while (--len >= 0) |
261 | { |
262 | assert(len >= 0); |
263 | mLangLookup[a][b][len] = old[len]; |
264 | } |
265 | free(old); |
266 | } |
267 | else |
268 | { |
269 | mLangLookup[a][b] = gralloc<const IsoLangEntry *>(2); |
270 | if (!mLangLookup[a][b]) continue; |
271 | mLangLookup[a][b][1] = NULL; |
272 | mLangLookup[a][b][0] = &LANG_ENTRIES[i]; |
273 | } |
274 | } |
275 | while (2 * mSeedPosition < maxIndex) |
276 | mSeedPosition *= 2; |
277 | }; |
278 | ~Locale2Lang() |
279 | { |
280 | for (int i = 0; i != 26; ++i) |
281 | for (int j = 0; j != 26; ++j) |
282 | free(mLangLookup[i][j]); |
283 | } |
284 | unsigned short getMsId(const char * locale) const |
285 | { |
286 | size_t length = strlen(locale); |
287 | size_t langLength = length; |
288 | const char * language = locale; |
289 | const char * script = NULL; |
290 | const char * region = NULL; |
291 | size_t regionLength = 0; |
292 | const char * dash = strchr(locale, '-'); |
293 | if (dash && (dash != locale)) |
294 | { |
295 | langLength = (dash - locale); |
296 | size_t nextPartLength = length - langLength - 1; |
297 | if (nextPartLength >= 2) |
298 | { |
299 | script = ++dash; |
300 | dash = strchr(dash, '-'); |
301 | if (dash) |
302 | { |
303 | nextPartLength = (dash - script); |
304 | region = ++dash; |
305 | } |
306 | if (nextPartLength == 2 && |
307 | (locale[langLength+1] > 0x40) && (locale[langLength+1] < 0x5B) && |
308 | (locale[langLength+2] > 0x40) && (locale[langLength+2] < 0x5B)) |
309 | { |
310 | region = script; |
311 | regionLength = nextPartLength; |
312 | script = NULL; |
313 | } |
314 | else if (nextPartLength == 4) |
315 | { |
316 | if (dash) |
317 | { |
318 | dash = strchr(dash, '-'); |
319 | if (dash) |
320 | { |
321 | nextPartLength = (dash - region); |
322 | } |
323 | else |
324 | { |
325 | nextPartLength = langLength - (region - locale); |
326 | } |
327 | regionLength = nextPartLength; |
328 | } |
329 | } |
330 | } |
331 | } |
332 | size_t a = 'e' - 'a'; |
333 | size_t b = 'n' - 'a'; |
334 | unsigned short langId = 0; |
335 | int i = 0; |
336 | switch (langLength) |
337 | { |
338 | case 2: |
339 | { |
340 | a = language[0] - 'a'; |
341 | b = language[1] - 'a'; |
342 | if ((a < 26) && (b < 26) && mLangLookup[a][b]) |
343 | { |
344 | while (mLangLookup[a][b][i]) |
345 | { |
346 | if (mLangLookup[a][b][i]->maLangStr[2] != '\0') |
347 | { |
348 | ++i; |
349 | continue; |
350 | } |
351 | if (region && (strncmp(mLangLookup[a][b][i]->maCountry, region, regionLength) == 0)) |
352 | { |
353 | langId = mLangLookup[a][b][i]->mnLang; |
354 | break; |
355 | } |
356 | else if (langId == 0) |
357 | { |
358 | // possible fallback code |
359 | langId = mLangLookup[a][b][i]->mnLang; |
360 | } |
361 | ++i; |
362 | } |
363 | } |
364 | } |
365 | break; |
366 | case 3: |
367 | { |
368 | a = language[0] - 'a'; |
369 | b = language[1] - 'a'; |
370 | if (mLangLookup[a][b]) |
371 | { |
372 | while (mLangLookup[a][b][i]) |
373 | { |
374 | if (mLangLookup[a][b][i]->maLangStr[2] != language[2]) |
375 | { |
376 | ++i; |
377 | continue; |
378 | } |
379 | if (region && (strncmp(mLangLookup[a][b][i]->maCountry, region, regionLength) == 0)) |
380 | { |
381 | langId = mLangLookup[a][b][i]->mnLang; |
382 | break; |
383 | } |
384 | else if (langId == 0) |
385 | { |
386 | // possible fallback code |
387 | langId = mLangLookup[a][b][i]->mnLang; |
388 | } |
389 | ++i; |
390 | } |
391 | } |
392 | } |
393 | break; |
394 | default: |
395 | break; |
396 | } |
397 | if (langId == 0) langId = 0x409; |
398 | return langId; |
399 | } |
400 | const IsoLangEntry * findEntryById(unsigned short langId) const |
401 | { |
402 | static const int maxIndex = sizeof(LANG_ENTRIES)/sizeof(IsoLangEntry); |
403 | int window = mSeedPosition; |
404 | int guess = mSeedPosition - 1; |
405 | while (LANG_ENTRIES[guess].mnLang != langId) |
406 | { |
407 | window /= 2; |
408 | if (window == 0) return NULL; |
409 | guess += (LANG_ENTRIES[guess].mnLang > langId)? -window : window; |
410 | while (guess >= maxIndex) |
411 | { |
412 | window /= 2; |
413 | guess -= window; |
414 | assert(window); |
415 | } |
416 | } |
417 | return &LANG_ENTRIES[guess]; |
418 | } |
419 | |
420 | CLASS_NEW_DELETE; |
421 | |
422 | private: |
423 | const IsoLangEntry ** mLangLookup[26][26]; |
424 | int mSeedPosition; |
425 | }; |
426 | |
427 | } // namespace graphite2 |
428 | |