blob: 4f8c44b58eea1dcf7f3eb9549273dea432bf510c [file] [log] [blame]
Hung-Te Lin76c55b22015-03-31 14:47:14 +08001#!/usr/bin/python2
2# Copyright 2015 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Authoritative source for Chromium OS region/locale configuration.
7
8Run this module to display all known regions (use --help to see options).
9"""
10
11from __future__ import print_function
12
13import argparse
14import collections
15import json
16import re
17import sys
18
19import yaml
20
21
22# The regular expression to check values in Region.keyboards and Region.locales.
23# Keyboards should come with xkb: protocol, or the input methods (ime:, m17n:).
24# Examples: xkb:us:intl:eng, ime:ime:zh-t:cangjie
25KEYBOARD_PATTERN = re.compile(r'^xkb:\w+:\w*:\w+$|'
26 r'^(ime|m17n|t13n):[\w:-]+$')
27# Locale should be a combination of language and location.
28# Examples: en-US, ja.
29LOCALE_PATTERN = re.compile(r'^(\w+)(-[A-Z0-9]+)?$')
30
31
32class Enum(frozenset):
33 """An enumeration type.
34
35 Usage:
36 To create a enum object:
37 dummy_enum = Enum(['A', 'B', 'C'])
38
39 To access a enum object, use:
40 dummy_enum.A
41 dummy_enum.B
42 """
43
44 def __getattr__(self, name):
45 if name in self:
46 return name
47 raise AttributeError
48
49
50class RegionException(Exception):
51 """Exception in Region handling."""
52 pass
53
54
55def MakeList(value):
56 """Converts the given value to a list.
57
58 Returns:
59 A list of elements from "value" if it is iterable (except string);
60 otherwise, a list contains only one element.
61 """
62 if (isinstance(value, collections.Iterable) and
63 not isinstance(value, basestring)):
64 return list(value)
65 return [value]
66
67
68class Region(object):
69 """Comprehensive, standard locale configuration per country/region.
70
71 See :ref:`regions-values` for detailed information on how to set these values.
72 """
73 # pylint gets confused by some of the docstrings.
74 # pylint: disable=C0322
75
76 # ANSI = US-like
77 # ISO = UK-like
78 # JIS = Japanese
79 # KS = Korean
80 # ABNT2 = Brazilian (like ISO but with an extra key to the left of the
81 # right shift key)
82 KeyboardMechanicalLayout = Enum(['ANSI', 'ISO', 'JIS', 'KS', 'ABNT2'])
83
84 region_code = None
85 """A unique identifier for the region. This may be a lower-case
86 `ISO 3166-1 alpha-2 code
87 <http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2>`_ (e.g., ``us``),
88 a variant within an alpha-2 entity (e.g., ``ca.fr``), or an
89 identifier for a collection of countries or entities (e.g.,
90 ``latam-es-419`` or ``nordic``). See :ref:`region-codes`.
91
92 Note that ``uk`` is not a valid identifier; ``gb`` is used as it is
93 the real alpha-2 code for the UK."""
94
95 keyboards = None
96 """A list of keyboard layout identifiers (e.g., ``xkb:us:intl:eng``
97 or ``m17n:ar``). This field was designed to be the physical keyboard layout
98 in the beginning, and then becomes a list of OOBE keyboard selection, which
99 then includes non-physical layout elements like input methods (``ime:``).
100 To avoid confusion, physical layout is now defined by
101 :py:attr:`keyboard_mechanical_layout`, and this is reserved for logical
102 layouts.
103
104 This is identical to the legacy VPD ``keyboard_layout`` value."""
105
106 time_zones = None
107 """A list of default `tz database time zone
108 <http://en.wikipedia.org/wiki/List_of_tz_database_time_zones>`_
109 identifiers (e.g., ``America/Los_Angeles``). See
110 `timezone_settings.cc <http://goo.gl/WSVUeE>`_ for supported time
111 zones.
112
113 This is identical to the legacy VPD ``initial_timezone`` value."""
114
115 locales = None
116 """A list of default locale codes (e.g., ``en-US``); see
117 `l10n_util.cc <http://goo.gl/kVkht>`_ for supported locales.
118
119 This is identital to the legacy VPD ``initial_locale`` field."""
120
121 keyboard_mechanical_layout = None
122 """The keyboard's mechanical layout (``ANSI`` [US-like], ``ISO``
123 [UK-like], ``JIS`` [Japanese], ``ABNT2`` [Brazilian] or ``KS`` [Korean])."""
124
125 description = None
126 """A human-readable description of the region.
127 This defaults to :py:attr:`region_code` if not set."""
128
129 notes = None
130 """Implementation notes about the region. This may be None."""
131
132 numeric_id = None
133 """An integer for mapping into Chrome OS HWID.
134 Please never change this once it is assigned."""
135
Hung-Te Lin436b6cc2015-04-03 12:15:47 +0800136 regulatory_domain = None
137 """An ISO 3166-1 alpha 2 upper-cased two-letter region code for setting
138 Wireless regulatory. See crosbug.com/p/38745 for more details.
139
140 When omitted, this will derive from region_code."""
141
Hung-Te Lin76c55b22015-03-31 14:47:14 +0800142 FIELDS = ['region_code', 'keyboards', 'time_zones', 'locales', 'description',
Hung-Te Lin436b6cc2015-04-03 12:15:47 +0800143 'keyboard_mechanical_layout', 'numeric_id', 'regulatory_domain']
Hung-Te Lin76c55b22015-03-31 14:47:14 +0800144 """Names of fields that define the region."""
145
146
147 def __init__(self, region_code, keyboards, time_zones, locales,
148 keyboard_mechanical_layout, description=None, notes=None,
Hung-Te Lin436b6cc2015-04-03 12:15:47 +0800149 numeric_id=None, regdomain=None):
Hung-Te Lin76c55b22015-03-31 14:47:14 +0800150 """Constructor.
151
152 Args:
153 region_code: See :py:attr:`region_code`.
154 keyboards: See :py:attr:`keyboards`. A single string is accepted for
155 backward compatibility.
156 time_zones: See :py:attr:`time_zones`.
157 locales: See :py:attr:`locales`. A single string is accepted
158 for backward compatibility.
159 keyboard_mechanical_layout: See :py:attr:`keyboard_mechanical_layout`.
160 description: See :py:attr:`description`.
161 notes: See :py:attr:`notes`.
162 numeric_id: See :py:attr:`numeric_id`. This must be None or a
163 non-negative integer.
Hung-Te Lin436b6cc2015-04-03 12:15:47 +0800164 regdomain: See :py:attr:`regulatory_domain`.
Hung-Te Lin76c55b22015-03-31 14:47:14 +0800165 """
Hung-Te Lin436b6cc2015-04-03 12:15:47 +0800166
167 def regdomain_from_region(region):
168 if region.find('.') >= 0:
169 region = region[:region.index('.')]
170 if len(region) == 2:
171 return region.upper()
172 return None
173
Hung-Te Lin76c55b22015-03-31 14:47:14 +0800174 # Quick check: should be 'gb', not 'uk'
175 if region_code == 'uk':
176 raise RegionException("'uk' is not a valid region code (use 'gb')")
177
178 self.region_code = region_code
179 self.keyboards = MakeList(keyboards)
180 self.time_zones = MakeList(time_zones)
181 self.locales = MakeList(locales)
182 self.keyboard_mechanical_layout = keyboard_mechanical_layout
183 self.description = description or region_code
184 self.notes = notes
185 self.numeric_id = numeric_id
Hung-Te Lin436b6cc2015-04-03 12:15:47 +0800186 self.regulatory_domain = (regdomain or regdomain_from_region(region_code))
Hung-Te Lin76c55b22015-03-31 14:47:14 +0800187
188 if self.numeric_id is not None:
189 if not isinstance(self.numeric_id, int):
190 raise TypeError('Numeric ID is %r but should be an integer' %
191 (self.numeric_id,))
192 if self.numeric_id < 0:
193 raise ValueError('Numeric ID is %r but should be non-negative' %
194 self.numeric_id)
195
196 for f in (self.keyboards, self.locales):
197 assert all(isinstance(x, str) for x in f), (
198 'Expected a list of strings, not %r' % f)
199 for f in self.keyboards:
200 assert KEYBOARD_PATTERN.match(f), (
201 'Keyboard pattern %r does not match %r' % (
202 f, KEYBOARD_PATTERN.pattern))
203 for f in self.locales:
204 assert LOCALE_PATTERN.match(f), (
205 'Locale %r does not match %r' % (
206 f, LOCALE_PATTERN.pattern))
Hung-Te Lin436b6cc2015-04-03 12:15:47 +0800207 assert (self.regulatory_domain and
208 len(self.regulatory_domain) == 2 and
209 self.regulatory_domain.upper() == self.regulatory_domain), (
210 "Regulatory domain settings error for region %s" % region_code)
Hung-Te Lin76c55b22015-03-31 14:47:14 +0800211
212 def __repr__(self):
213 return 'Region(%s)' % (', '.join([getattr(self, x) for x in self.FIELDS]))
214
215 def GetFieldsDict(self):
216 """Returns a dict of all substantive fields.
217
218 notes and description are excluded.
219 """
220 return dict((k, getattr(self, k)) for k in self.FIELDS)
221
222_KML = Region.KeyboardMechanicalLayout
223REGIONS_LIST = [
224 Region('au', 'xkb:us::eng', 'Australia/Sydney', 'en-AU', _KML.ANSI,
225 'Australia', None, 1),
226 Region('be', 'xkb:be::nld', 'Europe/Brussels', 'en-GB', _KML.ISO, 'Belgium',
227 'Flemish (Belgian Dutch) keyboard; British English language for '
228 'neutrality', 2),
229 Region('br', 'xkb:br::por', 'America/Sao_Paulo', 'pt-BR', _KML.ABNT2,
230 'Brazil (ABNT2)',
231 ('ABNT2 = ABNT NBR 10346 variant 2. This is the preferred layout '
232 'for Brazil. ABNT2 is mostly an ISO layout, but it 12 keys between '
233 'the shift keys; see http://goo.gl/twA5tq'), 3),
234 Region('br.abnt', 'xkb:br::por', 'America/Sao_Paulo', 'pt-BR', _KML.ISO,
235 'Brazil (ABNT)',
236 ('Like ABNT2, but lacking the extra key to the left of the right '
237 'shift key found in that layout. ABNT2 (the "br" region) is '
238 'preferred to this layout'), 4),
239 Region('br.usintl', 'xkb:us:intl:eng', 'America/Sao_Paulo', 'pt-BR',
240 _KML.ANSI, 'Brazil (US Intl)',
241 'Brazil with US International keyboard layout. ABNT2 ("br") and '
242 'ABNT1 ("br.abnt1 ") are both preferred to this.', 5),
243 Region('ca.ansi', 'xkb:us::eng', 'America/Toronto', 'en-CA', _KML.ANSI,
244 'Canada (US keyboard)',
245 'Canada with US (ANSI) keyboard. Not for en/fr hybrid ANSI '
246 'keyboards; for that you would want ca.hybridansi. See '
247 'http://goto/cros-canada', 6),
248 Region('ca.fr', 'xkb:ca::fra', 'America/Toronto', 'fr-CA', _KML.ISO,
249 'Canada (French keyboard)',
250 ('Canadian French (ISO) keyboard. The most common configuration for '
251 'Canadian French SKUs. See http://goto/cros-canada'), 7),
252 Region('ca.hybrid', 'xkb:ca:eng:eng', 'America/Toronto', 'en-CA', _KML.ISO,
253 'Canada (hybrid ISO)',
254 ('Canada with hybrid (ISO) xkb:ca:eng:eng + xkb:ca::fra keyboard, '
255 'defaulting to English language and keyboard. Used only if there '
256 'needs to be a single SKU for all of Canada. See '
257 'http://goto/cros-canada'), 8),
258 Region('ca.hybridansi', 'xkb:ca:eng:eng', 'America/Toronto', 'en-CA',
259 _KML.ANSI, 'Canada (hybrid ANSI)',
260 ('Canada with hybrid (ANSI) xkb:ca:eng:eng + xkb:ca::fra keyboard, '
261 'defaulting to English language and keyboard. Used only if there '
262 'needs to be a single SKU for all of Canada. See '
263 'http://goto/cros-canada'), 9),
264 Region('ca.multix', 'xkb:ca:multix:fra', 'America/Toronto', 'fr-CA',
265 _KML.ISO, 'Canada (multilingual)',
266 ("Canadian Multilingual keyboard; you probably don't want this. See "
267 'http://goto/cros-canada'), 10),
268 Region('ch', 'xkb:ch::ger', 'Europe/Zurich', 'en-US', _KML.ISO,
269 'Switzerland',
270 'German keyboard, but US English to be language-neutral; used in '
271 'the common case that there is only a single Swiss SKU.', 11),
272 Region('de', 'xkb:de::ger', 'Europe/Berlin', 'de', _KML.ISO, 'Germany',
273 None, 12),
274 Region('es', 'xkb:es::spa', 'Europe/Madrid', 'es', _KML.ISO, 'Spain',
275 None, 13),
276 Region('fi', 'xkb:fi::fin', 'Europe/Helsinki', 'fi', _KML.ISO, 'Finland',
277 None, 14),
278 Region('fr', 'xkb:fr::fra', 'Europe/Paris', 'fr', _KML.ISO, 'France',
279 None, 15),
280 Region('gb', 'xkb:gb:extd:eng', 'Europe/London', 'en-GB', _KML.ISO, 'UK',
281 None, 16),
282 Region('ie', 'xkb:gb:extd:eng', 'Europe/Dublin', 'en-GB', _KML.ISO,
283 'Ireland', None, 17),
284 Region('in', 'xkb:us::eng', 'Asia/Calcutta', 'en-US', _KML.ANSI, 'India',
285 None, 18),
286 Region('it', 'xkb:it::ita', 'Europe/Rome', 'it', _KML.ISO, 'Italy',
287 None, 19),
288 Region('latam-es-419', 'xkb:es::spa', 'America/Mexico_City', 'es-419',
289 _KML.ISO, 'Hispanophone Latin America',
290 ('Spanish-speaking countries in Latin America, using the Iberian '
291 '(Spain) Spanish keyboard, which is increasingly dominant in '
292 'Latin America. Known to be correct for '
293 'Chile, Colombia, Mexico, Peru; '
294 'still unconfirmed for other es-419 countries. The old Latin '
295 'American layout (xkb:latam::spa) has not been approved; before '
296 'using that you must seek review through http://goto/vpdsettings. '
297 'See also http://goo.gl/Iffuqh. Note that 419 is the UN M.49 '
Hung-Te Lin436b6cc2015-04-03 12:15:47 +0800298 'region code for Latin America'), 20, 'MX'),
Hung-Te Lin76c55b22015-03-31 14:47:14 +0800299 Region('my', 'xkb:us::eng', 'Asia/Kuala_Lumpur', 'ms', _KML.ANSI,
300 'Malaysia', None, 21),
301 Region('nl', 'xkb:us:intl:eng', 'Europe/Amsterdam', 'nl', _KML.ANSI,
302 'Netherlands', None, 22),
303 Region('nordic', 'xkb:se::swe', 'Europe/Stockholm', 'en-US', _KML.ISO,
304 'Nordics',
305 ('Unified SKU for Sweden, Norway, and Denmark. This defaults '
306 'to Swedish keyboard layout, but starts with US English language '
307 'for neutrality. Use if there is a single combined SKU for Nordic '
Hung-Te Lin436b6cc2015-04-03 12:15:47 +0800308 'countries.'), 23, 'SE'),
Hung-Te Lin76c55b22015-03-31 14:47:14 +0800309 Region('nz', 'xkb:us::eng', 'Pacific/Auckland', 'en-NZ', _KML.ANSI,
310 'New Zealand', None, 24),
311 Region('ph', 'xkb:us::eng', 'Asia/Manila', 'en-US', _KML.ANSI,
312 'Philippines', None, 25),
313 Region('ru', 'xkb:ru::rus', 'Europe/Moscow', 'ru', _KML.ANSI, 'Russia',
314 'For R31+ only; R30 and earlier must use US keyboard for login', 26),
315 Region('se', 'xkb:se::swe', 'Europe/Stockholm', 'sv', _KML.ISO, 'Sweden',
316 ('Use this if there separate SKUs for Nordic countries (Sweden, '
317 'Norway, and Denmark), or the device is only shipping to Sweden. '
318 "If there is a single unified SKU, use 'nordic' instead."), 27),
319 Region('sg', 'xkb:us::eng', 'Asia/Singapore', 'en-GB', _KML.ANSI,
320 'Singapore', None, 28),
321 Region('us', 'xkb:us::eng', 'America/Los_Angeles', 'en-US', _KML.ANSI,
322 'United States', None, 29),
323 Region('jp', 'xkb:jp::jpn', 'Asia/Tokyo', 'ja', _KML.JIS,
324 'Japan', None, 30),
Hung-Te Linb6203bf2015-04-10 10:43:30 +0800325 Region('za', 'xkb:us:intl:eng', 'Africa/Johannesburg', 'en-ZA', _KML.ANSI,
326 'South Africa', None, 31),
Hung-Te Lin76c55b22015-03-31 14:47:14 +0800327 Region('hk',
328 ['xkb:us::eng', 'ime:zh-t:cangjie', 'ime:zh-t:quick',
329 'ime:zh-t:array', 'ime:zh-t:dayi', 'ime:zh-t:zhuyin',
330 'ime:zh-t:pinyin'],
331 'Asia/Hong_Kong', ['zh-TW', 'en-GB', 'zh-CN'], _KML.ANSI,
332 'Hong Kong', None, 33),
333 Region('cz', ['xkb:cz::cze', 'xkb:cz:qwerty:cze'], 'Europe/Prague',
334 ['cs', 'en-GB'], _KML.ISO, 'Czech Republic', None, 35),
Hung-Te Linb6203bf2015-04-10 10:43:30 +0800335 Region('th', ['xkb:us::eng', 'm17n:th', 'm17n:th_pattajoti', 'm17n:th_tis'],
336 'Asia/Bangkok', ['th', 'en-GB'], _KML.ANSI, 'Thailand', None, 36),
Hung-Te Lin76c55b22015-03-31 14:47:14 +0800337 Region('tw',
338 ['xkb:us::eng', 'ime:zh-t:zhuyin', 'ime:zh-t:array',
339 'ime:zh-t:dayi', 'ime:zh-t:cangjie', 'ime:zh-t:quick',
340 'ime:zh-t:pinyin'],
341 'Asia/Taipei', ['zh-TW', 'en-US'], _KML.ANSI, 'Taiwan', None, 38),
342 Region('pl', 'xkb:pl::pol', 'Europe/Warsaw', ['pl', 'en-GB'], _KML.ANSI,
343 'Poland', None, 39),
344]
345"""A list of :py:class:`regions.Region` objects for
346all **confirmed** regions. A confirmed region is a region whose
347properties are known to be correct and may be used to launch a device."""
348
349
350UNCONFIRMED_REGIONS_LIST = []
351"""A list of :py:class:`regions.Region` objects for
352**unconfirmed** regions. These are believed to be correct but
353unconfirmed, and all fields should be verified (and the row moved into
354:py:data:`regions.Region.REGIONS_LIST`) before
355launch. See <http://goto/vpdsettings>.
356
357Currently, non-Latin keyboards must use an underlying Latin keyboard
358for VPD. (This assumption should be revisited when moving items to
359:py:data:`regions.Region.REGIONS_LIST`.) This is
360currently being discussed on <http://crbug.com/325389>.
361
362Some timezones may be missing from ``timezone_settings.cc`` (see
363http://crosbug.com/p/23902). This must be rectified before moving
364items to :py:data:`regions.Region.REGIONS_LIST`.
365"""
366
367INCOMPLETE_REGIONS_LIST = []
368"""A list of :py:class:`regions.Region` objects for
369**incomplete** regions. These may contain incorrect information, and all
370fields must be reviewed before launch. See http://goto/vpdsettings.
371"""
372
373
374def ConsolidateRegions(regions):
375 """Consolidates a list of regions into a dict.
376
377 Args:
378 regions: A list of Region objects. All objects for any given
379 region code must be identical or we will throw an exception.
380 (We allow duplicates in case identical region objects are
381 defined in both regions.py and the overlay, e.g., when moving
382 items to the public overlay.)
383
384 Returns:
385 A dict from region code to Region.
386
387 Raises:
388 RegionException: If there are multiple regions defined for a given
389 region, and the values for those regions differ.
390 """
391 # Build a dict from region_code to the first Region with that code.
392 region_dict = {}
393 for r in regions:
394 existing_region = region_dict.get(r.region_code)
395 if existing_region:
396 if existing_region.GetFieldsDict() != r.GetFieldsDict():
397 raise RegionException(
398 'Conflicting definitions for region %r: %r, %r' %
399 (r.region_code, existing_region.GetFieldsDict(),
400 r.GetFieldsDict()))
401 else:
402 region_dict[r.region_code] = r
403
404 return region_dict
405
406
407def BuildRegionsDict(include_all=False):
408 """Builds a dictionary mapping from code to :py:class:`regions.Region` object.
409
410 The regions include:
411
412 * :py:data:`regions.REGIONS_LIST`
413 * :py:data:`regions_overlay.REGIONS_LIST`
414 * Only if ``include_all`` is true:
415
416 * :py:data:`regions.UNCONFIRMED_REGIONS_LIST`
417 * :py:data:`regions.INCOMPLETE_REGIONS_LIST`
418
419 A region may only appear in one of the above lists, or this function
420 will (deliberately) fail.
421 """
422 regions = list(REGIONS_LIST)
423 if include_all:
424 regions += UNCONFIRMED_REGIONS_LIST + INCOMPLETE_REGIONS_LIST
425
426 # Build dictionary of region code to list of regions with that
427 # region code. Check manually for duplicates, since the region may
428 # be present both in the overlay and the public repo.
429 return ConsolidateRegions(regions)
430
431
432REGIONS = BuildRegionsDict()
433
434
435def main(args=sys.argv[1:], out=None):
436 parser = argparse.ArgumentParser(description=(
437 'Display all known regions and their parameters. '))
438 parser.add_argument('--format',
439 choices=('human-readable', 'csv', 'json', 'yaml'),
440 default='human-readable',
441 help='Output format (default=%(default)s)')
442 parser.add_argument('--all', action='store_true',
443 help='Include unconfirmed and incomplete regions')
444 parser.add_argument('--output', default=None,
445 help='Specify output file')
446 args = parser.parse_args(args)
447
448 regions_dict = BuildRegionsDict(args.all)
449 if out is None:
450 if args.output is None:
451 out = sys.stdout
452 else:
453 out = open(args.output, 'w')
454
455 # Handle YAML and JSON output.
456 if args.format == 'yaml' or args.format == 'json':
457 data = {}
458 for region in regions_dict.values():
459 item = {}
460 for field in Region.FIELDS:
461 item[field] = getattr(region, field)
462 data[region.region_code] = item
463 if args.format == 'yaml':
464 yaml.dump(data, out)
465 else:
466 json.dump(data, out)
467 return
468
469 # Handle CSV or plain-text output: build a list of lines to print.
470 lines = [Region.FIELDS]
471
472 def CoerceToString(value):
473 """Returns the arguments in simple string type.
474
475 If value is a list, concatenate its values with commas. Otherwise, just
476 return value.
477 """
478 if isinstance(value, list):
479 return ','.join(value)
480 else:
481 return str(value)
482 for region in sorted(regions_dict.values(), key=lambda v: v.region_code):
483 lines.append([CoerceToString(getattr(region, field))
484 for field in Region.FIELDS])
485
486 if args.format == 'csv':
487 # Just print the lines in CSV format.
488 for l in lines:
489 print(','.join(l))
490 elif args.format == 'human-readable':
491 num_columns = len(lines[0])
492
493 # Calculate maximum length of each column.
494 max_lengths = []
495 for column_no in xrange(num_columns):
496 max_lengths.append(max(len(line[column_no]) for line in lines))
497
498 # Print each line, padding as necessary to the max column length.
499 for line in lines:
500 for column_no in xrange(num_columns):
501 out.write(line[column_no].ljust(max_lengths[column_no] + 2))
502 out.write('\n')
503 else:
504 exit('Sorry, unknown format specified: %s' % args.format)
505
506
507if __name__ == '__main__':
508 main()