blob: 4aab96bbc73289adf90398c46e5060c6fe6d5d91 [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),
325 Region('hk',
326 ['xkb:us::eng', 'ime:zh-t:cangjie', 'ime:zh-t:quick',
327 'ime:zh-t:array', 'ime:zh-t:dayi', 'ime:zh-t:zhuyin',
328 'ime:zh-t:pinyin'],
329 'Asia/Hong_Kong', ['zh-TW', 'en-GB', 'zh-CN'], _KML.ANSI,
330 'Hong Kong', None, 33),
331 Region('cz', ['xkb:cz::cze', 'xkb:cz:qwerty:cze'], 'Europe/Prague',
332 ['cs', 'en-GB'], _KML.ISO, 'Czech Republic', None, 35),
333 Region('tw',
334 ['xkb:us::eng', 'ime:zh-t:zhuyin', 'ime:zh-t:array',
335 'ime:zh-t:dayi', 'ime:zh-t:cangjie', 'ime:zh-t:quick',
336 'ime:zh-t:pinyin'],
337 'Asia/Taipei', ['zh-TW', 'en-US'], _KML.ANSI, 'Taiwan', None, 38),
338 Region('pl', 'xkb:pl::pol', 'Europe/Warsaw', ['pl', 'en-GB'], _KML.ANSI,
339 'Poland', None, 39),
340]
341"""A list of :py:class:`regions.Region` objects for
342all **confirmed** regions. A confirmed region is a region whose
343properties are known to be correct and may be used to launch a device."""
344
345
346UNCONFIRMED_REGIONS_LIST = []
347"""A list of :py:class:`regions.Region` objects for
348**unconfirmed** regions. These are believed to be correct but
349unconfirmed, and all fields should be verified (and the row moved into
350:py:data:`regions.Region.REGIONS_LIST`) before
351launch. See <http://goto/vpdsettings>.
352
353Currently, non-Latin keyboards must use an underlying Latin keyboard
354for VPD. (This assumption should be revisited when moving items to
355:py:data:`regions.Region.REGIONS_LIST`.) This is
356currently being discussed on <http://crbug.com/325389>.
357
358Some timezones may be missing from ``timezone_settings.cc`` (see
359http://crosbug.com/p/23902). This must be rectified before moving
360items to :py:data:`regions.Region.REGIONS_LIST`.
361"""
362
363INCOMPLETE_REGIONS_LIST = []
364"""A list of :py:class:`regions.Region` objects for
365**incomplete** regions. These may contain incorrect information, and all
366fields must be reviewed before launch. See http://goto/vpdsettings.
367"""
368
369
370def ConsolidateRegions(regions):
371 """Consolidates a list of regions into a dict.
372
373 Args:
374 regions: A list of Region objects. All objects for any given
375 region code must be identical or we will throw an exception.
376 (We allow duplicates in case identical region objects are
377 defined in both regions.py and the overlay, e.g., when moving
378 items to the public overlay.)
379
380 Returns:
381 A dict from region code to Region.
382
383 Raises:
384 RegionException: If there are multiple regions defined for a given
385 region, and the values for those regions differ.
386 """
387 # Build a dict from region_code to the first Region with that code.
388 region_dict = {}
389 for r in regions:
390 existing_region = region_dict.get(r.region_code)
391 if existing_region:
392 if existing_region.GetFieldsDict() != r.GetFieldsDict():
393 raise RegionException(
394 'Conflicting definitions for region %r: %r, %r' %
395 (r.region_code, existing_region.GetFieldsDict(),
396 r.GetFieldsDict()))
397 else:
398 region_dict[r.region_code] = r
399
400 return region_dict
401
402
403def BuildRegionsDict(include_all=False):
404 """Builds a dictionary mapping from code to :py:class:`regions.Region` object.
405
406 The regions include:
407
408 * :py:data:`regions.REGIONS_LIST`
409 * :py:data:`regions_overlay.REGIONS_LIST`
410 * Only if ``include_all`` is true:
411
412 * :py:data:`regions.UNCONFIRMED_REGIONS_LIST`
413 * :py:data:`regions.INCOMPLETE_REGIONS_LIST`
414
415 A region may only appear in one of the above lists, or this function
416 will (deliberately) fail.
417 """
418 regions = list(REGIONS_LIST)
419 if include_all:
420 regions += UNCONFIRMED_REGIONS_LIST + INCOMPLETE_REGIONS_LIST
421
422 # Build dictionary of region code to list of regions with that
423 # region code. Check manually for duplicates, since the region may
424 # be present both in the overlay and the public repo.
425 return ConsolidateRegions(regions)
426
427
428REGIONS = BuildRegionsDict()
429
430
431def main(args=sys.argv[1:], out=None):
432 parser = argparse.ArgumentParser(description=(
433 'Display all known regions and their parameters. '))
434 parser.add_argument('--format',
435 choices=('human-readable', 'csv', 'json', 'yaml'),
436 default='human-readable',
437 help='Output format (default=%(default)s)')
438 parser.add_argument('--all', action='store_true',
439 help='Include unconfirmed and incomplete regions')
440 parser.add_argument('--output', default=None,
441 help='Specify output file')
442 args = parser.parse_args(args)
443
444 regions_dict = BuildRegionsDict(args.all)
445 if out is None:
446 if args.output is None:
447 out = sys.stdout
448 else:
449 out = open(args.output, 'w')
450
451 # Handle YAML and JSON output.
452 if args.format == 'yaml' or args.format == 'json':
453 data = {}
454 for region in regions_dict.values():
455 item = {}
456 for field in Region.FIELDS:
457 item[field] = getattr(region, field)
458 data[region.region_code] = item
459 if args.format == 'yaml':
460 yaml.dump(data, out)
461 else:
462 json.dump(data, out)
463 return
464
465 # Handle CSV or plain-text output: build a list of lines to print.
466 lines = [Region.FIELDS]
467
468 def CoerceToString(value):
469 """Returns the arguments in simple string type.
470
471 If value is a list, concatenate its values with commas. Otherwise, just
472 return value.
473 """
474 if isinstance(value, list):
475 return ','.join(value)
476 else:
477 return str(value)
478 for region in sorted(regions_dict.values(), key=lambda v: v.region_code):
479 lines.append([CoerceToString(getattr(region, field))
480 for field in Region.FIELDS])
481
482 if args.format == 'csv':
483 # Just print the lines in CSV format.
484 for l in lines:
485 print(','.join(l))
486 elif args.format == 'human-readable':
487 num_columns = len(lines[0])
488
489 # Calculate maximum length of each column.
490 max_lengths = []
491 for column_no in xrange(num_columns):
492 max_lengths.append(max(len(line[column_no]) for line in lines))
493
494 # Print each line, padding as necessary to the max column length.
495 for line in lines:
496 for column_no in xrange(num_columns):
497 out.write(line[column_no].ljust(max_lengths[column_no] + 2))
498 out.write('\n')
499 else:
500 exit('Sorry, unknown format specified: %s' % args.format)
501
502
503if __name__ == '__main__':
504 main()