blob: 889917180d67a9ff5a6fcfe706dfc0983cd9cfd5 [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
136 FIELDS = ['region_code', 'keyboards', 'time_zones', 'locales', 'description',
137 'keyboard_mechanical_layout', 'numeric_id']
138 """Names of fields that define the region."""
139
140
141 def __init__(self, region_code, keyboards, time_zones, locales,
142 keyboard_mechanical_layout, description=None, notes=None,
143 numeric_id=None):
144 """Constructor.
145
146 Args:
147 region_code: See :py:attr:`region_code`.
148 keyboards: See :py:attr:`keyboards`. A single string is accepted for
149 backward compatibility.
150 time_zones: See :py:attr:`time_zones`.
151 locales: See :py:attr:`locales`. A single string is accepted
152 for backward compatibility.
153 keyboard_mechanical_layout: See :py:attr:`keyboard_mechanical_layout`.
154 description: See :py:attr:`description`.
155 notes: See :py:attr:`notes`.
156 numeric_id: See :py:attr:`numeric_id`. This must be None or a
157 non-negative integer.
158 """
159 # Quick check: should be 'gb', not 'uk'
160 if region_code == 'uk':
161 raise RegionException("'uk' is not a valid region code (use 'gb')")
162
163 self.region_code = region_code
164 self.keyboards = MakeList(keyboards)
165 self.time_zones = MakeList(time_zones)
166 self.locales = MakeList(locales)
167 self.keyboard_mechanical_layout = keyboard_mechanical_layout
168 self.description = description or region_code
169 self.notes = notes
170 self.numeric_id = numeric_id
171
172 if self.numeric_id is not None:
173 if not isinstance(self.numeric_id, int):
174 raise TypeError('Numeric ID is %r but should be an integer' %
175 (self.numeric_id,))
176 if self.numeric_id < 0:
177 raise ValueError('Numeric ID is %r but should be non-negative' %
178 self.numeric_id)
179
180 for f in (self.keyboards, self.locales):
181 assert all(isinstance(x, str) for x in f), (
182 'Expected a list of strings, not %r' % f)
183 for f in self.keyboards:
184 assert KEYBOARD_PATTERN.match(f), (
185 'Keyboard pattern %r does not match %r' % (
186 f, KEYBOARD_PATTERN.pattern))
187 for f in self.locales:
188 assert LOCALE_PATTERN.match(f), (
189 'Locale %r does not match %r' % (
190 f, LOCALE_PATTERN.pattern))
191
192 def __repr__(self):
193 return 'Region(%s)' % (', '.join([getattr(self, x) for x in self.FIELDS]))
194
195 def GetFieldsDict(self):
196 """Returns a dict of all substantive fields.
197
198 notes and description are excluded.
199 """
200 return dict((k, getattr(self, k)) for k in self.FIELDS)
201
202_KML = Region.KeyboardMechanicalLayout
203REGIONS_LIST = [
204 Region('au', 'xkb:us::eng', 'Australia/Sydney', 'en-AU', _KML.ANSI,
205 'Australia', None, 1),
206 Region('be', 'xkb:be::nld', 'Europe/Brussels', 'en-GB', _KML.ISO, 'Belgium',
207 'Flemish (Belgian Dutch) keyboard; British English language for '
208 'neutrality', 2),
209 Region('br', 'xkb:br::por', 'America/Sao_Paulo', 'pt-BR', _KML.ABNT2,
210 'Brazil (ABNT2)',
211 ('ABNT2 = ABNT NBR 10346 variant 2. This is the preferred layout '
212 'for Brazil. ABNT2 is mostly an ISO layout, but it 12 keys between '
213 'the shift keys; see http://goo.gl/twA5tq'), 3),
214 Region('br.abnt', 'xkb:br::por', 'America/Sao_Paulo', 'pt-BR', _KML.ISO,
215 'Brazil (ABNT)',
216 ('Like ABNT2, but lacking the extra key to the left of the right '
217 'shift key found in that layout. ABNT2 (the "br" region) is '
218 'preferred to this layout'), 4),
219 Region('br.usintl', 'xkb:us:intl:eng', 'America/Sao_Paulo', 'pt-BR',
220 _KML.ANSI, 'Brazil (US Intl)',
221 'Brazil with US International keyboard layout. ABNT2 ("br") and '
222 'ABNT1 ("br.abnt1 ") are both preferred to this.', 5),
223 Region('ca.ansi', 'xkb:us::eng', 'America/Toronto', 'en-CA', _KML.ANSI,
224 'Canada (US keyboard)',
225 'Canada with US (ANSI) keyboard. Not for en/fr hybrid ANSI '
226 'keyboards; for that you would want ca.hybridansi. See '
227 'http://goto/cros-canada', 6),
228 Region('ca.fr', 'xkb:ca::fra', 'America/Toronto', 'fr-CA', _KML.ISO,
229 'Canada (French keyboard)',
230 ('Canadian French (ISO) keyboard. The most common configuration for '
231 'Canadian French SKUs. See http://goto/cros-canada'), 7),
232 Region('ca.hybrid', 'xkb:ca:eng:eng', 'America/Toronto', 'en-CA', _KML.ISO,
233 'Canada (hybrid ISO)',
234 ('Canada with hybrid (ISO) xkb:ca:eng:eng + xkb:ca::fra keyboard, '
235 'defaulting to English language and keyboard. Used only if there '
236 'needs to be a single SKU for all of Canada. See '
237 'http://goto/cros-canada'), 8),
238 Region('ca.hybridansi', 'xkb:ca:eng:eng', 'America/Toronto', 'en-CA',
239 _KML.ANSI, 'Canada (hybrid ANSI)',
240 ('Canada with hybrid (ANSI) xkb:ca:eng:eng + xkb:ca::fra keyboard, '
241 'defaulting to English language and keyboard. Used only if there '
242 'needs to be a single SKU for all of Canada. See '
243 'http://goto/cros-canada'), 9),
244 Region('ca.multix', 'xkb:ca:multix:fra', 'America/Toronto', 'fr-CA',
245 _KML.ISO, 'Canada (multilingual)',
246 ("Canadian Multilingual keyboard; you probably don't want this. See "
247 'http://goto/cros-canada'), 10),
248 Region('ch', 'xkb:ch::ger', 'Europe/Zurich', 'en-US', _KML.ISO,
249 'Switzerland',
250 'German keyboard, but US English to be language-neutral; used in '
251 'the common case that there is only a single Swiss SKU.', 11),
252 Region('de', 'xkb:de::ger', 'Europe/Berlin', 'de', _KML.ISO, 'Germany',
253 None, 12),
254 Region('es', 'xkb:es::spa', 'Europe/Madrid', 'es', _KML.ISO, 'Spain',
255 None, 13),
256 Region('fi', 'xkb:fi::fin', 'Europe/Helsinki', 'fi', _KML.ISO, 'Finland',
257 None, 14),
258 Region('fr', 'xkb:fr::fra', 'Europe/Paris', 'fr', _KML.ISO, 'France',
259 None, 15),
260 Region('gb', 'xkb:gb:extd:eng', 'Europe/London', 'en-GB', _KML.ISO, 'UK',
261 None, 16),
262 Region('ie', 'xkb:gb:extd:eng', 'Europe/Dublin', 'en-GB', _KML.ISO,
263 'Ireland', None, 17),
264 Region('in', 'xkb:us::eng', 'Asia/Calcutta', 'en-US', _KML.ANSI, 'India',
265 None, 18),
266 Region('it', 'xkb:it::ita', 'Europe/Rome', 'it', _KML.ISO, 'Italy',
267 None, 19),
268 Region('latam-es-419', 'xkb:es::spa', 'America/Mexico_City', 'es-419',
269 _KML.ISO, 'Hispanophone Latin America',
270 ('Spanish-speaking countries in Latin America, using the Iberian '
271 '(Spain) Spanish keyboard, which is increasingly dominant in '
272 'Latin America. Known to be correct for '
273 'Chile, Colombia, Mexico, Peru; '
274 'still unconfirmed for other es-419 countries. The old Latin '
275 'American layout (xkb:latam::spa) has not been approved; before '
276 'using that you must seek review through http://goto/vpdsettings. '
277 'See also http://goo.gl/Iffuqh. Note that 419 is the UN M.49 '
278 'region code for Latin America'), 20),
279 Region('my', 'xkb:us::eng', 'Asia/Kuala_Lumpur', 'ms', _KML.ANSI,
280 'Malaysia', None, 21),
281 Region('nl', 'xkb:us:intl:eng', 'Europe/Amsterdam', 'nl', _KML.ANSI,
282 'Netherlands', None, 22),
283 Region('nordic', 'xkb:se::swe', 'Europe/Stockholm', 'en-US', _KML.ISO,
284 'Nordics',
285 ('Unified SKU for Sweden, Norway, and Denmark. This defaults '
286 'to Swedish keyboard layout, but starts with US English language '
287 'for neutrality. Use if there is a single combined SKU for Nordic '
288 'countries.'), 23),
289 Region('nz', 'xkb:us::eng', 'Pacific/Auckland', 'en-NZ', _KML.ANSI,
290 'New Zealand', None, 24),
291 Region('ph', 'xkb:us::eng', 'Asia/Manila', 'en-US', _KML.ANSI,
292 'Philippines', None, 25),
293 Region('ru', 'xkb:ru::rus', 'Europe/Moscow', 'ru', _KML.ANSI, 'Russia',
294 'For R31+ only; R30 and earlier must use US keyboard for login', 26),
295 Region('se', 'xkb:se::swe', 'Europe/Stockholm', 'sv', _KML.ISO, 'Sweden',
296 ('Use this if there separate SKUs for Nordic countries (Sweden, '
297 'Norway, and Denmark), or the device is only shipping to Sweden. '
298 "If there is a single unified SKU, use 'nordic' instead."), 27),
299 Region('sg', 'xkb:us::eng', 'Asia/Singapore', 'en-GB', _KML.ANSI,
300 'Singapore', None, 28),
301 Region('us', 'xkb:us::eng', 'America/Los_Angeles', 'en-US', _KML.ANSI,
302 'United States', None, 29),
303 Region('jp', 'xkb:jp::jpn', 'Asia/Tokyo', 'ja', _KML.JIS,
304 'Japan', None, 30),
305 Region('hk',
306 ['xkb:us::eng', 'ime:zh-t:cangjie', 'ime:zh-t:quick',
307 'ime:zh-t:array', 'ime:zh-t:dayi', 'ime:zh-t:zhuyin',
308 'ime:zh-t:pinyin'],
309 'Asia/Hong_Kong', ['zh-TW', 'en-GB', 'zh-CN'], _KML.ANSI,
310 'Hong Kong', None, 33),
311 Region('cz', ['xkb:cz::cze', 'xkb:cz:qwerty:cze'], 'Europe/Prague',
312 ['cs', 'en-GB'], _KML.ISO, 'Czech Republic', None, 35),
313 Region('tw',
314 ['xkb:us::eng', 'ime:zh-t:zhuyin', 'ime:zh-t:array',
315 'ime:zh-t:dayi', 'ime:zh-t:cangjie', 'ime:zh-t:quick',
316 'ime:zh-t:pinyin'],
317 'Asia/Taipei', ['zh-TW', 'en-US'], _KML.ANSI, 'Taiwan', None, 38),
318 Region('pl', 'xkb:pl::pol', 'Europe/Warsaw', ['pl', 'en-GB'], _KML.ANSI,
319 'Poland', None, 39),
320]
321"""A list of :py:class:`regions.Region` objects for
322all **confirmed** regions. A confirmed region is a region whose
323properties are known to be correct and may be used to launch a device."""
324
325
326UNCONFIRMED_REGIONS_LIST = []
327"""A list of :py:class:`regions.Region` objects for
328**unconfirmed** regions. These are believed to be correct but
329unconfirmed, and all fields should be verified (and the row moved into
330:py:data:`regions.Region.REGIONS_LIST`) before
331launch. See <http://goto/vpdsettings>.
332
333Currently, non-Latin keyboards must use an underlying Latin keyboard
334for VPD. (This assumption should be revisited when moving items to
335:py:data:`regions.Region.REGIONS_LIST`.) This is
336currently being discussed on <http://crbug.com/325389>.
337
338Some timezones may be missing from ``timezone_settings.cc`` (see
339http://crosbug.com/p/23902). This must be rectified before moving
340items to :py:data:`regions.Region.REGIONS_LIST`.
341"""
342
343INCOMPLETE_REGIONS_LIST = []
344"""A list of :py:class:`regions.Region` objects for
345**incomplete** regions. These may contain incorrect information, and all
346fields must be reviewed before launch. See http://goto/vpdsettings.
347"""
348
349
350def ConsolidateRegions(regions):
351 """Consolidates a list of regions into a dict.
352
353 Args:
354 regions: A list of Region objects. All objects for any given
355 region code must be identical or we will throw an exception.
356 (We allow duplicates in case identical region objects are
357 defined in both regions.py and the overlay, e.g., when moving
358 items to the public overlay.)
359
360 Returns:
361 A dict from region code to Region.
362
363 Raises:
364 RegionException: If there are multiple regions defined for a given
365 region, and the values for those regions differ.
366 """
367 # Build a dict from region_code to the first Region with that code.
368 region_dict = {}
369 for r in regions:
370 existing_region = region_dict.get(r.region_code)
371 if existing_region:
372 if existing_region.GetFieldsDict() != r.GetFieldsDict():
373 raise RegionException(
374 'Conflicting definitions for region %r: %r, %r' %
375 (r.region_code, existing_region.GetFieldsDict(),
376 r.GetFieldsDict()))
377 else:
378 region_dict[r.region_code] = r
379
380 return region_dict
381
382
383def BuildRegionsDict(include_all=False):
384 """Builds a dictionary mapping from code to :py:class:`regions.Region` object.
385
386 The regions include:
387
388 * :py:data:`regions.REGIONS_LIST`
389 * :py:data:`regions_overlay.REGIONS_LIST`
390 * Only if ``include_all`` is true:
391
392 * :py:data:`regions.UNCONFIRMED_REGIONS_LIST`
393 * :py:data:`regions.INCOMPLETE_REGIONS_LIST`
394
395 A region may only appear in one of the above lists, or this function
396 will (deliberately) fail.
397 """
398 regions = list(REGIONS_LIST)
399 if include_all:
400 regions += UNCONFIRMED_REGIONS_LIST + INCOMPLETE_REGIONS_LIST
401
402 # Build dictionary of region code to list of regions with that
403 # region code. Check manually for duplicates, since the region may
404 # be present both in the overlay and the public repo.
405 return ConsolidateRegions(regions)
406
407
408REGIONS = BuildRegionsDict()
409
410
411def main(args=sys.argv[1:], out=None):
412 parser = argparse.ArgumentParser(description=(
413 'Display all known regions and their parameters. '))
414 parser.add_argument('--format',
415 choices=('human-readable', 'csv', 'json', 'yaml'),
416 default='human-readable',
417 help='Output format (default=%(default)s)')
418 parser.add_argument('--all', action='store_true',
419 help='Include unconfirmed and incomplete regions')
420 parser.add_argument('--output', default=None,
421 help='Specify output file')
422 args = parser.parse_args(args)
423
424 regions_dict = BuildRegionsDict(args.all)
425 if out is None:
426 if args.output is None:
427 out = sys.stdout
428 else:
429 out = open(args.output, 'w')
430
431 # Handle YAML and JSON output.
432 if args.format == 'yaml' or args.format == 'json':
433 data = {}
434 for region in regions_dict.values():
435 item = {}
436 for field in Region.FIELDS:
437 item[field] = getattr(region, field)
438 data[region.region_code] = item
439 if args.format == 'yaml':
440 yaml.dump(data, out)
441 else:
442 json.dump(data, out)
443 return
444
445 # Handle CSV or plain-text output: build a list of lines to print.
446 lines = [Region.FIELDS]
447
448 def CoerceToString(value):
449 """Returns the arguments in simple string type.
450
451 If value is a list, concatenate its values with commas. Otherwise, just
452 return value.
453 """
454 if isinstance(value, list):
455 return ','.join(value)
456 else:
457 return str(value)
458 for region in sorted(regions_dict.values(), key=lambda v: v.region_code):
459 lines.append([CoerceToString(getattr(region, field))
460 for field in Region.FIELDS])
461
462 if args.format == 'csv':
463 # Just print the lines in CSV format.
464 for l in lines:
465 print(','.join(l))
466 elif args.format == 'human-readable':
467 num_columns = len(lines[0])
468
469 # Calculate maximum length of each column.
470 max_lengths = []
471 for column_no in xrange(num_columns):
472 max_lengths.append(max(len(line[column_no]) for line in lines))
473
474 # Print each line, padding as necessary to the max column length.
475 for line in lines:
476 for column_no in xrange(num_columns):
477 out.write(line[column_no].ljust(max_lengths[column_no] + 2))
478 out.write('\n')
479 else:
480 exit('Sorry, unknown format specified: %s' % args.format)
481
482
483if __name__ == '__main__':
484 main()