Skip to content

Commit b7144ce

Browse files
committed
PD-5445
1 parent c8bedc4 commit b7144ce

4 files changed

Lines changed: 146 additions & 3 deletions

File tree

PD-5321-angular-i18n-guidelines.md

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
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. **Use ICU for plural/select logic.**
26+
Keep pluralization and gender/select logic inside one translatable unit.
27+
5. **Use `i18n` metadata (`meaning|description@@id`) for clarity and stability.**
28+
Add translator context where ambiguity exists.
29+
6. **Do not use HTML structure to force translation segmentation.**
30+
If visual emphasis is needed, prefer styling that does not break translation context.
31+
32+
## Anti-pattern (do not use)
33+
34+
Fragmenting one message into multiple translatable chunks:
35+
36+
```html
37+
<p>
38+
<span i18n>You have</span>
39+
<strong>{{ workCount }}</strong>
40+
<span i18n>works pending review.</span>
41+
</p>
42+
```
43+
44+
Problems:
45+
46+
- Translators cannot reorder words naturally for other languages.
47+
- Grammar agreement can break.
48+
- Translation memory is fragmented.
49+
50+
## Preferred pattern
51+
52+
Single message with placeholders:
53+
54+
```html
55+
<p i18n="dashboard pending work message|Shown in works summary card@@pending_work_summary">
56+
You have {{ workCount }} works pending review.
57+
</p>
58+
```
59+
60+
## Additional examples
61+
62+
### 1) Paragraph-level content
63+
64+
**Bad (segmented):**
65+
66+
```html
67+
<p>
68+
<span i18n>Connect your account to import</span>
69+
<span i18n>works and affiliations</span>
70+
<span i18n>automatically.</span>
71+
</p>
72+
```
73+
74+
**Good (single paragraph unit):**
75+
76+
```html
77+
<p i18n="connection help text|Displayed on account setup screen@@connect_import_help">
78+
Connect your account to import works and affiliations automatically.
79+
</p>
80+
```
81+
82+
### 2) Dynamic values
83+
84+
**Good:**
85+
86+
```html
87+
<p i18n="search results count|Results summary above table@@search_results_summary">
88+
Showing {{ shown }} of {{ total }} results.
89+
</p>
90+
```
91+
92+
### 3) Pluralization (ICU)
93+
94+
**Good:**
95+
96+
```html
97+
<p i18n="notifications count|User inbox summary@@notifications_count">
98+
{count, plural, =0 {You have no notifications.} one {You have one notification.} other {You have # notifications.}}
99+
</p>
100+
```
101+
102+
### 4) Attributes and controls
103+
104+
```html
105+
<input
106+
i18n-placeholder="search field placeholder|Global search input@@global_search_placeholder"
107+
placeholder="Search by keyword"
108+
/>
109+
110+
<button i18n="save button label|Primary save action@@save_button">
111+
Save changes
112+
</button>
113+
```
114+
115+
## Implementation checklist (PR-level)
116+
117+
- [ ] Full paragraph or complete message is marked as one i18n unit.
118+
- [ ] No unnecessary message splitting for style-only reasons.
119+
- [ ] Dynamic values are placeholders within the same message.
120+
- [ ] Plural/select grammar uses ICU when needed.
121+
- [ ] `meaning|description@@id` metadata is present for non-obvious strings.
122+
- [ ] Content reads naturally if translated with different word order.
123+
124+
## Migration guidance
125+
126+
When touching existing templates:
127+
128+
1. Identify adjacent i18n strings that form one user-facing message.
129+
2. Merge them into a single i18n-marked element.
130+
3. Preserve styling without splitting translatable content (move styling to CSS classes where possible).
131+
4. Add metadata and stable custom IDs for translator context.
132+
5. Validate extraction output and review translation units for readability.
133+
134+
## Scope and exceptions
135+
136+
Valid exceptions where splitting is acceptable:
137+
138+
- Truly independent UI strings (e.g., separate labels, independent actions)
139+
- Reusable standalone components with isolated semantics
140+
- Accessibility-only text that serves a different purpose from visual copy
141+
142+
If uncertain, prefer a single message unit and ask in code review.
143+

src/app/account-settings/components/settings-security-alternate-sign-in/settings-security-alternate-sign-in.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export class SettingsSecurityAlternateSignInComponent
4242
$destroy = new Subject<void>()
4343
isMobile: any
4444
refreshAccounts$ = new BehaviorSubject<void>(undefined)
45-
authChallengeLabel = $localize`:@@accountSettings.security.unlinkTheAlternateSignInAccount:to unlink the alternate sign in account`
45+
authChallengeLabel = $localize`:@@accountSettings.security.unlinkTheAlternateSignInAccount:unlink the alternate sign in account`
4646
labelDeleteButton = $localize`:@@shared.delete:Delete`
4747

4848
constructor(

src/app/account-settings/components/settings-security-password/settings-security-password.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export class SettingsSecurityPasswordComponent implements OnInit, OnDestroy {
4848
currentValidateAtLeastALetterOrSymbolStatus: boolean
4949
currentValidateAtLeastANumber: boolean
5050
confirmPasswordPlaceholder = $localize`:@@accountSettings.security.password.confirmPasswordPlaceholder:Confirm your new password`
51-
authChallengeLabel = $localize`:@@accountSettings.security.password.authChallengeLabel:to complete your password reset`
51+
authChallengeLabel = $localize`:@@accountSettings.security.password.authChallengeLabel:complete your password reset`
5252
errorMatcher = new ErrorStateMatcherForTwoFactorFields()
5353

5454
constructor(

src/app/account-settings/components/settings-security-two-factor-auth/settings-security-two-factor-auth.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export class SettingsSecurityTwoFactorAuthComponent implements OnInit {
2828
form: UntypedFormGroup
2929
success = false
3030
cancel = false
31-
authChallengeLabel = $localize`:@@accountSettings.security.disable2FA:to disable 2FA`
31+
authChallengeLabel = $localize`:@@accountSettings.security.disable2FA:disable 2FA`
3232

3333
constructor(
3434
private _router: Router,

0 commit comments

Comments
 (0)