|
| 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 | + |
0 commit comments