Skip to content

Commit ce86f36

Browse files
committed
PD-5445
1 parent 0c1b174 commit ce86f36

2 files changed

Lines changed: 176 additions & 7 deletions

File tree

PD-5321-angular-i18n-guidelines.md

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# PD-5321: Angular i18n Guidelines (Paragraph-First)
2+
3+
## Why this guideline exists
4+
5+
Translations in this codebase currently mix two patterns:
6+
7+
- Legacy segmented strings (many small translation units for one sentence/paragraph)
8+
- Paragraph-level translation units (single cohesive message)
9+
10+
This inconsistency creates fragmented translation memory and extra translator effort.
11+
The standard moving forward is **paragraph-first translation**: translate complete user-facing thoughts as one unit whenever possible.
12+
13+
## Core principle
14+
15+
Use Angular i18n to mark **complete semantic messages** (typically full paragraphs, headings, labels, or complete button text), not fragmented pieces.
16+
17+
## Standard rules
18+
19+
1. **Translate full paragraphs as one unit.**
20+
Avoid splitting one sentence/paragraph across multiple elements solely for styling.
21+
2. **Keep meaning and grammar together.**
22+
Articles, nouns, verbs, punctuation, and dynamic placeholders that form a single thought should stay in one translation unit.
23+
3. **Use placeholders for dynamic values.**
24+
Interpolate variables in a single i18n-marked sentence.
25+
4. **Provide rich context metadata when dynamic values are present.**
26+
In the `description`, explain what each dynamic value represents so translators can select correct grammar and tone.
27+
5. **Use ICU for plural/select logic.**
28+
Keep pluralization and gender/select logic inside one translatable unit.
29+
6. **Use `i18n` metadata (`meaning|description@@id`) for clarity and stability.**
30+
Add translator context where ambiguity exists, especially for dynamic values.
31+
7. **Do not use HTML structure to force translation segmentation.**
32+
If visual emphasis is needed, prefer styling that does not break translation context.
33+
34+
## Metadata guideline for dynamic values
35+
36+
When using placeholders (for example `{{ total }}`, `{{ roleName }}`, `{{ startDate }}`), the i18n description should explicitly define each value.
37+
38+
### Recommended pattern
39+
40+
```html
41+
<p i18n="results summary|Displays list results count; shown=results currently visible on page; total=all records matching filters@@search_results_summary">
42+
Showing {{ shown }} of {{ total }} results.
43+
</p>
44+
```
45+
46+
### Avoid vague descriptions
47+
48+
- Bad: `i18n="message|results info@@results_info"`
49+
- Good: `i18n="message|shown=results displayed on current page; total=all results returned by query@@results_info"`
50+
51+
## Anti-pattern (do not use)
52+
53+
Fragmenting one message because part of it is visually emphasized (`<strong>`, link, badge, etc.):
54+
55+
```html
56+
<p>
57+
<span i18n>Access granted by</span>
58+
<strong>{{ giverName }}</strong>
59+
<span i18n>on your ORCID record.</span>
60+
</p>
61+
```
62+
63+
Problems:
64+
65+
- Translators cannot reorder words naturally for other languages.
66+
- Grammar agreement can break.
67+
- Translation memory is fragmented.
68+
69+
## Preferred pattern
70+
71+
Single message with placeholders and inline emphasis:
72+
73+
```html
74+
<p i18n="trusted party grant message|Message in trusted-party card; giverName=full name of user who granted trust@@trusted_party_grant_message">
75+
Access granted by <strong>{{ giverName }}</strong> on your ORCID record.
76+
</p>
77+
```
78+
79+
## Additional template examples
80+
81+
### 1) Paragraph-level content
82+
83+
**Bad (segmented for style):**
84+
85+
```html
86+
<p>
87+
<span i18n>Trusted individual:</span>
88+
<strong>{{ trustedIndividualName }}</strong>
89+
<span i18n>can now update your works.</span>
90+
</p>
91+
```
92+
93+
**Good (single paragraph unit, still styled):**
94+
95+
```html
96+
<p i18n="trusted party update rights|Shown in trusted individuals section; trustedIndividualName=full name of the trusted individual@@trusted_party_update_rights">
97+
Trusted individual: <strong>{{ trustedIndividualName }}</strong> can now update your works.
98+
</p>
99+
```
100+
101+
### 2) Dynamic values
102+
103+
**Good:**
104+
105+
```html
106+
<p i18n="search results count|Results summary above table; shown=visible results count; total=all query matches@@search_results_summary">
107+
Showing {{ shown }} of {{ total }} results.
108+
</p>
109+
```
110+
111+
### 3) Pluralization (ICU)
112+
113+
**Good:**
114+
115+
```html
116+
<p i18n="notifications count|User inbox summary; count=number of unread notifications@@notifications_count">
117+
{count, plural, =0 {You have no notifications.} one {You have one notification.} other {You have # notifications.}}
118+
</p>
119+
```
120+
121+
### 4) Attributes and controls
122+
123+
```html
124+
<input
125+
i18n-placeholder="search field placeholder|Global search input placeholder@@global_search_placeholder"
126+
placeholder="Search by keyword"
127+
/>
128+
129+
<button i18n="save button label|Primary save action@@save_button">
130+
Save changes
131+
</button>
132+
```
133+
134+
## TypeScript examples (`$localize`)
135+
136+
Use `$localize` with `meaning|description@@id` in TS files, following the same paragraph-first and context-rich rules.
137+
138+
### 1) Dynamic value in a single message
139+
140+
```ts
141+
const summary = $localize`:search results summary|shown=results currently visible in table; total=all records matching filters@@search_results_summary_ts:Showing ${shown}:shown: of ${total}:total: results.`;
142+
```
143+
144+
### 2) Error message with contextual placeholder
145+
146+
```ts
147+
const errorMessage = $localize`:permission error|roleName=display label of the required permission role@@permission_required_error:You need the ${roleName}:roleName: role to continue.`;
148+
```
149+
150+
### 3) Date range with explicit placeholder meaning
151+
152+
```ts
153+
const rangeMessage = $localize`:reporting range|startDate=report period start date; endDate=report period end date@@report_range_message:Report period: ${startDate}:startDate: to ${endDate}:endDate:.`;
154+
```
155+
156+
## Implementation checklist (PR-level)
157+
158+
- [ ] Full paragraph or complete message is marked as one i18n unit.
159+
- [ ] No unnecessary message splitting for style-only reasons.
160+
- [ ] Dynamic values are placeholders within the same message.
161+
- [ ] Dynamic-value messages include clear placeholder context in the i18n description.
162+
- [ ] Plural/select grammar uses ICU when needed.
163+
- [ ] `meaning|description@@id` metadata is present for non-obvious strings.
164+
- [ ] Content reads naturally if translated with different word order.
165+
166+
## Scope and exceptions
167+
168+
Valid exceptions where splitting is acceptable:
169+
170+
- Truly independent UI strings (e.g., separate labels, independent actions)
171+
- Reusable standalone components with isolated semantics
172+
- Accessibility-only text that serves a different purpose from visual copy
173+
174+
If uncertain, prefer a single message unit and ask in code review.
175+

projects/orcid-registry-ui/src/lib/components/auth-challenge/auth-challenge.component.html

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,7 @@ <h1 class="orc-font-heading-small text-center font-normal" i18n>
2525
>and</ng-container
2626
>&nbsp;}@if (showTwoFactorField) {<ng-container i18n
2727
>a two-factor authentication code</ng-container
28-
>&nbsp;}<ng-container
29-
i18n="
30-
auth challenge action clause|Clause in the verification sentence before
31-
a dynamic action fragment. actionDescription is a translated clause like
32-
'to disable 2FA', 'to unlink the alternate sign in account', or 'for'.
33-
Keep the sentence natural in your language.@@authChallenge.actionDescriptionClause"
34-
>{{ actionDescription }}</ng-container
28+
>&nbsp;}<ng-container>{{ actionDescription }}</ng-container
3529
>@if (boldText) {&nbsp;<b>{{ boldText }}</b
3630
>}@if (trailingText) {&nbsp;{{ trailingText }}}
3731
</p>

0 commit comments

Comments
 (0)