@iyulab/modern-app 0.47.0 → 0.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@ import { css as e } from "lit";
2
2
  //#region src/components/SidebarGroup.styles.ts
3
3
  var t = e`
4
4
  :host {
5
- /* SidebarLink 과 같은 원천 — 그룹 강조는 활성 링크와 같은 색 계열이어야 한다 */
5
+
6
6
  --group-accent: var(--app-sidebar-active-bg, var(--u-primary-color, #1976D2));
7
7
 
8
8
  display: flex;
@@ -19,7 +19,7 @@ var t = e`
19
19
  justify-content: space-between;
20
20
  gap: 12px;
21
21
  padding: 8px 12px;
22
- /* 호스트 하한(--u-target-size, 미설정 = 0) — 상자는 border-box(기반 스타일)라 여백을 포함한다. */
22
+
23
23
  min-height: var(--u-target-size, 0px);
24
24
  background-color: transparent;
25
25
  border: none;
@@ -33,10 +33,10 @@ var t = e`
33
33
  padding: 8px;
34
34
  }
35
35
  button[selected] {
36
- /* 패널 위 텍스트라 강조색을 그대로 쓰면 대비가 부족하다(blue-600 = 3.68:1).
37
- 기본은 활성 배경과 같은 파생식으로 한 단 어둡게 해 AA 를 넘긴다(4.85:1) — 활성 면이 «진한» 색이라는 전제다.
38
- 연한 활성 면(+ 진한 글자)을 쓰는 테마에서는 그 면을 15% 어둡게 한 색이 패널 위에서 1.6:1 로 묻혔다.
39
- 그런 테마는 --app-sidebar-group-active-fg 를 «패널 위 강조 글자» 로 정한다. */
36
+
37
+
38
+
39
+
40
40
  color: var(--app-sidebar-group-active-fg, color-mix(in srgb, var(--app-sidebar-active-bg, var(--u-primary-color, #1976D2)) 85%, black));
41
41
  }
42
42
  button:hover {
@@ -54,15 +54,15 @@ var t = e`
54
54
  .icon {
55
55
  flex-shrink: 0;
56
56
  color: inherit;
57
- /* 링크와 같은 토큰 — 한 레일의 아이콘은 한 크기다(위계는 들여쓰기·캐럿·레일이 진다). */
57
+
58
58
  font-size: var(--app-sidebar-icon-size, 20px);
59
59
  }
60
60
 
61
- /*
62
- * ★그룹도 자식 링크와 같은 label 단이다. 종전에는 굵기로 위계를 냈지만(600 vs 500),
63
- * 자식은 이미 **32px 들여쓰기 + 2px 레일 + 캐럿**으로 갈린다 — 굵기 한 단을 위계에
64
- * 더 쓰면 스케일 밖의 다섯 번째 값이 생긴다. 위계는 배치가 지고 단은 용도가 진다.
65
- */
61
+
62
+
63
+
64
+
65
+
66
66
  .label {
67
67
  flex: 1;
68
68
  font-size: var(--u-text-label-size, 13px);
@@ -99,10 +99,10 @@ var t = e`
99
99
  opacity: 0;
100
100
  }
101
101
 
102
- /*
103
- * 접힌 상태의 라벨 - 시각적으로만 숨긴다. 접근성 트리에는 남아 항목의 이름이 된다
104
- * (hidden 은 트리에서도 빼서 이름 없는 항목을 만들었다). 절대배치라 flex 배치와 gap 에 끼지 않는다.
105
- */
102
+
103
+
104
+
105
+
106
106
  [part~='label'][compact] {
107
107
  position: absolute;
108
108
  width: 1px;
@@ -2,10 +2,10 @@ import { css as e } from "lit";
2
2
  //#region src/components/SidebarLink.styles.ts
3
3
  var t = e`
4
4
  :host {
5
- /* 활성 배경의 단일 원천. 소비자는 셸 계약 토큰(--app-sidebar-active-bg) 또는
6
- 역할 토큰(--u-primary-color) 어느 쪽을 덮어도 hover 파생까지 함께 따라온다.
7
- 팔레트(--u-blue-600)를 직접 읽으면 브랜드를 바꾸려는 소비자가 "진짜 파랑"이
8
- 필요한 배지·차트까지 함께 오염시키는 길밖에 없다. */
5
+
6
+
7
+
8
+
9
9
  --link-active-bg: var(--app-sidebar-active-bg, var(--u-primary-color, #1976D2));
10
10
 
11
11
  display: block;
@@ -38,7 +38,7 @@ var t = e`
38
38
  justify-content: flex-start;
39
39
  gap: var(--app-sidebar-item-gap, 12px);
40
40
  padding: var(--app-sidebar-item-padding, 8px 12px);
41
- /* 호스트 하한(--u-target-size, 미설정 = 0). */
41
+
42
42
  box-sizing: border-box;
43
43
  min-height: var(--u-target-size, 0px);
44
44
  }
@@ -53,14 +53,14 @@ var t = e`
53
53
  color: var(--link-icon-color, inherit);
54
54
  font-size: var(--app-sidebar-icon-size, 20px);
55
55
  }
56
- /* Selected always wins over any consumer-supplied --link-icon-color — a
57
- * high-contrast icon on the active background matters more than a brand tint. */
56
+
57
+
58
58
  :host([selected]) u-icon {
59
59
  color: var(--app-sidebar-active-icon-color, var(--app-sidebar-active-fg, var(--u-txt-color-inverse, #FFFFFF)));
60
60
  }
61
61
 
62
- /* 활성 표시 막대 — 시작 변(좌→우 언어에서 왼쪽). 기본 색은 활성 면과 같아 보이지 않는다(기본 외형 불변).
63
- 밝은 활성 면처럼 면 색만으로 현재 위치를 말하지 않는 셸이 위치를 한 번 더 표시하는 자리다. */
62
+
63
+
64
64
  :host([selected]) .container::before {
65
65
  content: '';
66
66
  position: absolute;
@@ -71,11 +71,11 @@ var t = e`
71
71
  background: var(--app-sidebar-active-indicator-color, var(--app-sidebar-active-bg, var(--u-primary-color, #1976D2)));
72
72
  }
73
73
 
74
- /*
75
- * ★내비 항목은 «대상의 짧은 이름» = label 단이다(용도 배정).
76
- * ⚠행 높이는 바뀌지 않는다: 13 x 1.5 = 19.5px < 아이콘 20px 이라 행을 잡는 것은
77
- * 여전히 아이콘이다. 접힌 사이드바(아이콘만)와 펼친 사이드바의 행 높이가 같아야 한다.
78
- */
74
+
75
+
76
+
77
+
78
+
79
79
  span {
80
80
  flex: 1;
81
81
  font-size: var(--u-text-label-size, 13px);
@@ -86,11 +86,11 @@ var t = e`
86
86
  text-overflow: ellipsis;
87
87
  }
88
88
 
89
- /*
90
- * 접힌 상태의 라벨 - 시각적으로만 숨긴다. 접근성 트리에는 남아 항목의 이름이 된다
91
- * (hidden 은 트리에서도 빼서 이름 없는 항목을 만들었다). 절대배치라 flex 배치와 gap 에 끼지 않는다.
92
- */
93
- /* 새 창 알림 — 화면에는 없고 링크의 접근성 이름에만 붙는다(KWCAG 7.2.1). */
89
+
90
+
91
+
92
+
93
+
94
94
  .new-tab-hint,
95
95
  [part~='label'][compact] {
96
96
  position: absolute;
@@ -104,8 +104,8 @@ var t = e`
104
104
  border: 0;
105
105
  }
106
106
 
107
- /* 건수 — 라벨 끝, 숫자는 표 숫자처럼 자릿수가 흔들리지 않게. 접힌 사이드바에서는 라벨처럼
108
- 시각적으로만 숨긴다(접근 가능한 이름에는 남는다). */
107
+
108
+
109
109
  .count {
110
110
  flex: none;
111
111
  margin-inline-start: auto;
@@ -14,14 +14,14 @@ var t = e`
14
14
  padding: 8px 12px 4px;
15
15
  }
16
16
 
17
- /* 섹션 제목·부제는 본문보다 약한 두 단이다 — 제목 = 보조 글자(--u-txt-color-weak),
18
- 부제 = 3단 글자(--u-txt-color-weaker, components 2.0). 종전에는 역할 층에 AA 를 넘는
19
- 3단이 없어 팔레트(neutral-700/600)를 직접 읽었고, 그래서 테마가 이 두 줄에 닿지 않았다. */
20
- /*
21
- * ★크기·굵기·자간은 스케일의 단으로 옮겼다(용도 배정).
22
- * ⚠ overline 에 대문자 변환을 붙이지 않는다 — 이 줄에 ellipsis 가 있어 잘림이 늘고,
23
- * 섹션 이름은 소비자가 쓴 글자다(CJK 에는 효과도 없다).
24
- */
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
25
  .title {
26
26
  color: var(--u-txt-color-weak, #616161);
27
27
  font-size: var(--u-text-overline-size, 11px);
@@ -7,7 +7,7 @@ var t = e`
7
7
  column-gap: var(--u-space-md, 12px);
8
8
  }
9
9
 
10
- /* The rail: a marker, then a line down to the next entry's marker. */
10
+
11
11
  .rail {
12
12
  display: flex;
13
13
  flex-direction: column;
@@ -22,11 +22,11 @@ var t = e`
22
22
  box-sizing: border-box;
23
23
  width: var(--app-timeline-marker-size, 12px);
24
24
  height: var(--app-timeline-marker-size, 12px);
25
- /* Centred on the heading's first line (label size × leading). */
25
+
26
26
  margin-top: calc((var(--u-text-label-size, 13px) * var(--u-text-label-leading, 1.5) - var(--app-timeline-marker-size, 12px)) / 2);
27
27
  border-radius: 50%;
28
- /* Fill and the colour on it come as a pair (the components contract — warning carries dark text,
29
- not white). The edge is the strong step: a yellow fill alone is about 1.4:1 against the page. */
28
+
29
+
30
30
  background-color: var(--_marker, var(--u-border-color-strong, #BDBDBD));
31
31
  color: var(--_on-marker, var(--u-txt-color, #212121));
32
32
  border: 1px solid var(--_edge, var(--u-border-color-strong, #BDBDBD));
@@ -68,10 +68,10 @@ var t = e`
68
68
  flex: 1 1 auto;
69
69
  min-height: var(--u-space-md, 12px);
70
70
  margin-top: var(--u-space-2xs, 4px);
71
- /* The rail is what reads as a timeline — the weak border step (about 1.25:1 on a card) vanished. */
71
+
72
72
  border-left: 1px solid var(--u-border-color-strong, #BDBDBD);
73
73
  }
74
- /* The last entry has no next marker to reach. */
74
+
75
75
  :host(:last-child) .line {
76
76
  display: none;
77
77
  }
@@ -113,7 +113,7 @@ var t = e`
113
113
  color: var(--u-txt-color-weak, #616161);
114
114
  overflow-wrap: anywhere;
115
115
  }
116
- /* No body content — no gap under the header. */
116
+
117
117
  .body.empty {
118
118
  display: none;
119
119
  }
@@ -3,8 +3,8 @@ import { css as e } from "lit";
3
3
  var t = e`
4
4
  :host {
5
5
  display: flex;
6
- /* 위치 지정 — 안의 숨김 문구(절대 위치 · 1px)의 포함 블록이 이 요소가 된다. 없으면 포함 블록이 문서 전체라 호스트의
7
- overflow 감싸개를 건너뛰고 그 정적 위치에서 문서를 늘렸다(위치 지정되지 않은 스크롤 상자 안에서). */
6
+
7
+
8
8
  position: relative;
9
9
  flex-direction: row;
10
10
  gap: var(--u-space-xl, 20px);
@@ -42,7 +42,7 @@ var t = e`
42
42
  background: none;
43
43
  border: none;
44
44
  padding: var(--u-space-2xs, 4px) 0;
45
- /* 호스트 하한(--u-target-size, 미설정 = 0). */
45
+
46
46
  min-height: var(--u-target-size, 0px);
47
47
  font: inherit;
48
48
  color: var(--u-txt-color-weak, #616161);
@@ -53,9 +53,9 @@ var t = e`
53
53
  color: var(--u-txt-color-disabled, #BDBDBD);
54
54
  cursor: not-allowed;
55
55
  }
56
- /* 도달은 못 하지만(linear 모드에서 아직 안 지나온 스텝) disabled 는 아니다 —
57
- 소비자가 미리 그 존재는 보여주고 싶을 수 있어 시각적으로 흐리되 마우스 커서는
58
- "막혔다"가 아니라 "여기는 아직"으로 남긴다. */
56
+
57
+
58
+
59
59
  .step[aria-disabled="true"]:not([disabled]) {
60
60
  cursor: default;
61
61
  opacity: 0.6;
@@ -100,9 +100,9 @@ var t = e`
100
100
  gap: var(--u-space-sm, 8px);
101
101
  }
102
102
 
103
- /* live region — 화면엔 안 보이지만 스크린리더는 읽는다. 폭/높이는 디자인 토큰이 아니라
104
- "존재는 하되 렌더 크기는 0에 가까워야" 하는 구조값이라 var() 로 감싼다(리터럴 스캔
105
- 예외 — 시트 자체와 무관한 값이라 시트에 실을 이유가 없다). */
103
+
104
+
105
+
106
106
  .announcement {
107
107
  position: absolute;
108
108
  overflow: hidden;
package/dist/index.d.ts CHANGED
@@ -22,7 +22,7 @@ export type { MasterDetailLayoutEventMap } from './components/MasterDetailLayout
22
22
  export { Wizard } from './components/Wizard.js';
23
23
  export type { WizardStep, WizardStepState, WizardStepChangeDetail, WizardEventMap } from './components/Wizard.js';
24
24
  export { translate } from './translate.js';
25
- export { modernAppLocale, registerLocale, setDefaultLocale, getLocaleStrings, getDefaultLocale, } from './internals/locale.js';
25
+ export { modernAppLocale, getLocaleStrings, getDefaultLocale, } from './internals/locale.js';
26
26
  export type { ModernAppLocaleStrings, ModernAppMessageKey } from './internals/locale.js';
27
27
  export { ScreenObserver } from './internals/ScreenObserver.js';
28
28
  export type { ScreenSize, ScreenObserverConfig, ScreenResizeEvent } from './internals/ScreenObserver.js';
package/dist/index.js CHANGED
@@ -1,20 +1,20 @@
1
1
  import { ScreenObserver as e } from "./internals/ScreenObserver.js";
2
- import { getDefaultLocale as t, getLocaleStrings as n, modernAppLocale as r, registerLocale as i, setDefaultLocale as a } from "./internals/locale.js";
3
- import { EmptyState as o } from "./components/EmptyState.js";
4
- import { app as s } from "./App.js";
5
- import { filterSidebarItems as c } from "./layouts/filterSidebarItems.js";
6
- import { PageHeader as l } from "./components/PageHeader.js";
7
- import { GroupBox as u } from "./components/GroupBox.js";
8
- import { InfoSection as d } from "./components/InfoSection.js";
9
- import { InfoField as f, isBlank as p } from "./components/InfoField.js";
10
- import { TimelineItem as m } from "./components/TimelineItem.js";
11
- import { Timeline as h } from "./components/Timeline.js";
12
- import { ActionBar as g } from "./components/ActionBar.js";
13
- import { MasterDetailLayout as _ } from "./components/MasterDetailLayout.js";
14
- import { Wizard as v } from "./components/Wizard.js";
15
- import { translate as y } from "./translate.js";
16
- import { AccessDeniedError as b, ContentLoadError as x, ContentRenderError as S, NotFoundError as C, OutletMissingError as w, RouteError as T } from "@iyulab/router";
2
+ import { getDefaultLocale as t, getLocaleStrings as n, modernAppLocale as r } from "./internals/locale.js";
3
+ import { EmptyState as i } from "./components/EmptyState.js";
4
+ import { app as a } from "./App.js";
5
+ import { filterSidebarItems as o } from "./layouts/filterSidebarItems.js";
6
+ import { PageHeader as s } from "./components/PageHeader.js";
7
+ import { GroupBox as c } from "./components/GroupBox.js";
8
+ import { InfoSection as l } from "./components/InfoSection.js";
9
+ import { InfoField as u, isBlank as d } from "./components/InfoField.js";
10
+ import { TimelineItem as f } from "./components/TimelineItem.js";
11
+ import { Timeline as p } from "./components/Timeline.js";
12
+ import { ActionBar as m } from "./components/ActionBar.js";
13
+ import { MasterDetailLayout as h } from "./components/MasterDetailLayout.js";
14
+ import { Wizard as g } from "./components/Wizard.js";
15
+ import { translate as _ } from "./translate.js";
16
+ import { AccessDeniedError as v, ContentLoadError as y, ContentRenderError as b, NotFoundError as x, OutletMissingError as S, RouteError as C } from "@iyulab/router";
17
17
  //#region src/index.ts
18
- var E = s;
18
+ var w = a;
19
19
  //#endregion
20
- export { b as AccessDeniedError, g as ActionBar, x as ContentLoadError, S as ContentRenderError, o as EmptyState, u as GroupBox, f as InfoField, d as InfoSection, _ as MasterDetailLayout, C as NotFoundError, w as OutletMissingError, l as PageHeader, T as RouteError, e as ScreenObserver, h as Timeline, m as TimelineItem, v as Wizard, s as app, E as default, c as filterSidebarItems, t as getDefaultLocale, n as getLocaleStrings, p as isBlank, r as modernAppLocale, i as registerLocale, a as setDefaultLocale, y as translate };
20
+ export { v as AccessDeniedError, m as ActionBar, y as ContentLoadError, b as ContentRenderError, i as EmptyState, c as GroupBox, u as InfoField, l as InfoSection, h as MasterDetailLayout, x as NotFoundError, S as OutletMissingError, s as PageHeader, C as RouteError, e as ScreenObserver, p as Timeline, f as TimelineItem, g as Wizard, a as app, w as default, o as filterSidebarItems, t as getDefaultLocale, n as getLocaleStrings, d as isBlank, r as modernAppLocale, _ as translate };
@@ -7,9 +7,11 @@ import { EmptyStateVariant } from '../components/EmptyState.js';
7
7
  * 남의 외형이었고, 권한 거부(403)도 «장애» 처럼 보였다.
8
8
  *
9
9
  * - 403 → `no-access`(라우트 가드의 거부) · 404 → `not-found` · 그 밖 → `error`(오류 메시지가 설명).
10
- * - 판정은 클래스가 아니라 **코드**로 한다 — `AccessDeniedError`·`NotFoundError` 가 403·404 를 싣고, 라우트가 던진
11
- * `{ status: 404 }` 류도 router 가 같은 코드의 `RouteError` 로 감싼다. 진단 코드(`OUTLET_MISSING` 등)는 화면에
12
- * 코드로 내지 않고 메시지를 설명으로 쓴다.
10
+ * - 판정은 클래스가 아니라 **코드**로 한다 — `AccessDeniedError`·`NotFoundError` 가 403·404 를 싣고, 가드가 던진
11
+ * `{ status: 404 }` 류도 router 가 같은 코드의 `RouteError` 로 감싸며, `render()` 가 던진 `NotFoundError`
12
+ * (레코드 없음)는 router 0.21 부터 그대로 닿는다. 진단 코드(`OUTLET_MISSING` 등)는 화면에 코드로 내지 않고
13
+ * 메시지를 설명으로 쓴다.
14
+ * - 탭 제목은 화면 제목과 같다 — 오류 메시지(`Page not found: http://…`)는 진단이지 제목이 아니다.
13
15
  * - 소비자가 `fallback` 을 주면 그것이 그대로 이긴다(이 모듈은 쓰이지 않는다).
14
16
  */
15
17
  export declare function variantOf(error: Pick<RouteError, 'code'>): EmptyStateVariant;
@@ -8,9 +8,20 @@ function n(e) {
8
8
  default: return "error";
9
9
  }
10
10
  }
11
- var r = { render: (r) => {
12
- let i = new t();
13
- return i.variant = n(r.error), i.variant === "error" && (i.title = e().routeErrorTitle, i.description = r.error.message), i;
14
- } };
11
+ function r(t) {
12
+ let r = e();
13
+ switch (n(t.error)) {
14
+ case "no-access": return r.noAccessTitle;
15
+ case "not-found": return r.notFoundTitle;
16
+ default: return r.routeErrorTitle;
17
+ }
18
+ }
19
+ var i = {
20
+ title: r,
21
+ render: (e) => {
22
+ let i = new t();
23
+ return i.variant = n(e.error), i.variant === "error" && (i.title = r(e), i.description = e.error.message), i;
24
+ }
25
+ };
15
26
  //#endregion
16
- export { r as defaultFallback, n as variantOf };
27
+ export { i as defaultFallback, n as variantOf };
@@ -46,19 +46,7 @@ export type ModernAppMessageKey = keyof ModernAppLocaleStrings;
46
46
  /** The `modern-app` chrome-string namespace. Register translations here; pick the language with `Locale.set()`. */
47
47
  export declare const modernAppLocale: import('@iyulab/components/dist/utilities/Locale.js').LocaleNamespace<keyof ModernAppLocaleStrings>;
48
48
  /**
49
- * @deprecated Register with `modernAppLocale.register(lang, strings)` and choose the language with
50
- * `Locale.set()`. This forwards to the namespace; a function-valued `wizardStepAnnouncement` keeps
51
- * working (the namespace form is a `{index}`/`{total}`/`{label}` template).
52
- */
53
- export declare function registerLocale(lang: string, strings: Partial<ModernAppLocaleStrings>): void;
54
- /**
55
- * @deprecated Choose the language with `Locale.set()` of `@iyulab/components` — this package now
56
- * follows it. When called, this still takes precedence over `Locale` for elements without their
57
- * own `locale`; `setDefaultLocale(undefined)` hands the choice back to `Locale`.
58
- */
59
- export declare function setDefaultLocale(lang: string | undefined): void;
60
- /**
61
- * Resolve every chrome string for a language tag (empty → `setDefaultLocale` → active `Locale`).
49
+ * Resolve every chrome string for a language tag (empty → the active `Locale`).
62
50
  */
63
51
  export declare function getLocaleStrings(lang?: string): ModernAppLocaleStrings;
64
52
  /** The English defaults — for tests and reference. */
@@ -46,24 +46,12 @@ n.register("en", t), n.register("ko", {
46
46
  wizardNext: "다음",
47
47
  wizardStepAnnouncement: "{total}단계 중 {index}단계: {label}"
48
48
  });
49
- var r, i = /* @__PURE__ */ new Map(), a = "\0modern-app:announcer:";
50
- function o(e, t) {
51
- let { wizardStepAnnouncement: r, ...o } = t, s = { ...o };
52
- if (r) {
53
- let t = e.toLowerCase();
54
- i.set(t, r), s.wizardStepAnnouncement = a + t;
55
- }
56
- n.register(e, s);
49
+ function r(e, t, r) {
50
+ let i = n.textIn(e || void 0, t, r);
51
+ return i === t ? "" : i;
57
52
  }
58
- function s(e) {
59
- r = e;
60
- }
61
- function c(e, t, i) {
62
- let a = n.textIn(e || r, t, i);
63
- return a === t ? "" : a;
64
- }
65
- function l(e) {
66
- let t = (t) => c(e, t);
53
+ function i(e) {
54
+ let t = (t) => r(e, t);
67
55
  return {
68
56
  back: t("back"),
69
57
  noDataTitle: t("noDataTitle"),
@@ -85,21 +73,18 @@ function l(e) {
85
73
  pageLoading: t("pageLoading"),
86
74
  wizardBack: t("wizardBack"),
87
75
  wizardNext: t("wizardNext"),
88
- wizardStepAnnouncement: (t, n, r) => {
89
- let o = c(e, "wizardStepAnnouncement", {
90
- index: t,
91
- total: n,
92
- label: r
93
- });
94
- return o.startsWith(a) ? i.get(o.slice(22))(t, n, r) : o;
95
- }
76
+ wizardStepAnnouncement: (t, n, i) => r(e, "wizardStepAnnouncement", {
77
+ index: t,
78
+ total: n,
79
+ label: i
80
+ })
96
81
  };
97
82
  }
98
- function u() {
83
+ function a() {
99
84
  return {
100
85
  ...t,
101
86
  wizardStepAnnouncement: (e, t, n) => `Step ${e} of ${t}: ${n}`
102
87
  };
103
88
  }
104
89
  //#endregion
105
- export { u as getDefaultLocale, l as getLocaleStrings, n as modernAppLocale, o as registerLocale, s as setDefaultLocale };
90
+ export { a as getDefaultLocale, i as getLocaleStrings, n as modernAppLocale };