Speak the editor in each person's Home Assistant language: every text in the editor section of en.json, the mockup in the screens' language, and one Language & region card for all screens

- vue-i18n 11 with the Composition API: English comes with the page, any other language loads the first time it is
  needed from screen_manager/translations, and a text a language lacks shows in English. The build keeps only _meta
  and editor of each file (vite.config.ts), never the screens' or the add-on's texts.
- The editor follows the language of the user's Home Assistant profile (<html lang> of Home Assistant's page), else
  the browser's; plural forms follow the file's _meta.plural, with the screens' rules.
- Every request tells the add-on the editor's language (X-ESP-Screens-Language), so its own words can follow it.
- The mockup draws the screens' words (Unavailable, Page 2 ›, the date, "5 min ago") in the screens' language, with
  their clock and number format.
- Settings → Language & region: the screen language, time format and number format for every screen, with the user's
  own profile offered when it differs. The per-screen clock choice goes; the top bar points there. A screen that
  still needs the new language says so in its update badge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Max Gramser
2026-09-19 14:40:29 +02:00
parent c2cf3391d6
commit 1dc2eab0cf
58 changed files with 2573 additions and 672 deletions
+4 -2
View File
@@ -22,6 +22,8 @@ FIRMWARE = (ROOT / 'components/smart_display/header_bar.h').read_text()
FIRMWARE += json.dumps(json.loads((ROOT / 'screen_manager/translations/en.json').read_text(encoding='utf-8'))['screen']['time'])
TILES = (ROOT / 'components/smart_display/runtime_tiles.h').read_text()
EDITOR = (ROOT / 'web/src/model/topbar.ts').read_text()
# The mockup's words, in the editor's part of the translations since app 0.2.90.
EDITOR_WORDS = json.loads((ROOT / 'screen_manager/translations/en.json').read_text(encoding='utf-8'))['editor']['mockup']
PROFILES = ('guition-4848s040.yaml', 'home-like-2432s028.yaml')
def state(value, last_changed='2026-09-13T14:00:00+00:00', **attributes):
@@ -207,7 +209,7 @@ class ParityTests(unittest.TestCase):
for phrase in ('Just now', ' min ago', ' hour ago', ' hours ago', 'Yesterday', ' days ago', '1 week ago', ' weeks ago',
'1 month ago', ' months ago', ' year ago', ' years ago', 'In ', 'Tomorrow'):
self.assertIn(phrase, FIRMWARE)
self.assertIn(phrase, EDITOR)
self.assertIn(phrase, json.dumps(EDITOR_WORDS['time']))
for threshold in ('3600', '86400', '172800', '604800', '2592000', '31536000'):
self.assertIn(threshold, FIRMWARE)
self.assertIn(threshold, EDITOR)
@@ -215,7 +217,7 @@ class ParityTests(unittest.TestCase):
self.assertIn('Math.max(2, Math.floor((cap * 4 + 5) / 10)), item: Math.max(6, Math.floor((cap * 125 + 50) / 100)), name: Math.max(8, Math.floor((cap * 16 + 5) / 10))', EDITOR)
self.assertIn('width * 35 / 100', FIRMWARE)
self.assertIn('(metrics.width * 35) / 100', EDITOR)
self.assertEqual(header_bar.MONTHS, tuple(re.search(r'const MONTHS = \[(.*?)\];', EDITOR).group(1).replace('"', '').replace(' ', '').split(',')))
self.assertEqual(header_bar.MONTHS, tuple(EDITOR_WORDS['date']['months_short']))
def test_profiles_draw_the_bar_from_the_runtime(self):
for name in PROFILES: