diff --git a/scripts/template-text-baseline.json b/scripts/template-text-baseline.json index 549e795a8..0967ef424 100644 --- a/scripts/template-text-baseline.json +++ b/scripts/template-text-baseline.json @@ -1,17 +1 @@ -{ - "src/app/features/accounting/accounting-closure-form.component.ts": 2, - "src/app/features/accounting/accounting-rule-form.component.ts": 13, - "src/app/features/accounting/charges/charge-form.component.ts": 3, - "src/app/features/accounting/financial-activity-mapping-form.component.ts": 2, - "src/app/features/accounting/journal-entry-form.component.ts": 5, - "src/app/features/campaigns/email-messages/email-messages.component.ts": 1, - "src/app/features/campaigns/sms-campaigns/sms-campaign-form.component.ts": 1, - "src/app/features/fintech/asset-owner-view/asset-owner-view.component.ts": 2, - "src/app/features/interop/interop-transfers.component.ts": 1, - "src/app/features/loans/point-in-time/loans-point-in-time.component.ts": 5, - "src/app/features/loans/rescheduling/reschedule-form.component.ts": 10, - "src/app/features/products/savings-account-view.component.ts": 10, - "src/app/features/security/audit-logs/audit-logs-list.component.ts": 7, - "src/app/features/settings/working-days.component.ts": 4, - "src/app/features/transfers/standing-instruction-form.component.ts": 2 -} +{} diff --git a/src/app/features/accounting/accounting-closure-form.component.test.ts b/src/app/features/accounting/accounting-closure-form.component.test.ts index fdfa96c1f..b4f7ab23e 100644 --- a/src/app/features/accounting/accounting-closure-form.component.test.ts +++ b/src/app/features/accounting/accounting-closure-form.component.test.ts @@ -26,6 +26,7 @@ import { Router } from '@angular/router'; import { of, throwError } from 'rxjs'; import { provideTranslateTesting } from '../../testing/i18n-testing'; import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { expectLookedUp } from '../../testing/translated-text'; describe('AccountingClosureFormComponent', () => { let component: AccountingClosureFormComponent; @@ -104,4 +105,11 @@ describe('AccountingClosureFormComponent', () => { expect(closureApiSpy.create).not.toHaveBeenCalled(); expect(component.isSaving()).toBe(false); }); + + it('renders its heading and date label through the translation adapter', () => { + expectLookedUp(fixture.nativeElement, [ + 'ACCOUNTING_CLOSURES.CLOSE_ACCOUNTING_PERIOD', + 'ACCOUNTING_CLOSURES.CLOSING_DATE', + ]); + }); }); diff --git a/src/app/features/accounting/accounting-closure-form.component.ts b/src/app/features/accounting/accounting-closure-form.component.ts index f24d94657..b1d37c6db 100644 --- a/src/app/features/accounting/accounting-closure-form.component.ts +++ b/src/app/features/accounting/accounting-closure-form.component.ts @@ -21,7 +21,7 @@ import { Component, OnInit, inject, signal } from '@angular/core'; import { Router } from '@angular/router'; import { FormsModule } from '@angular/forms'; -import { ACCOUNTING_CLOSURE_API, OFFICE_API } from '../../core/adapters'; +import { ACCOUNTING_CLOSURE_API, OFFICE_API, TranslatePipe } from '../../core/adapters'; import type { Office } from '../../core/adapters'; import { HelpIconComponent } from '../../shared'; import { @@ -51,6 +51,7 @@ import { createPickersReady } from '../../shared/utils/pickers-ready'; standalone: true, imports: [ FormsModule, + TranslatePipe, HelpIconComponent, IonButton, IonSpinner, @@ -72,7 +73,7 @@ import { createPickersReady } from '../../shared/utils/pickers-ready'; - Close Accounting Period + {{ 'ACCOUNTING_CLOSURES.CLOSE_ACCOUNTING_PERIOD' | appTranslate }} @@ -98,7 +99,9 @@ import { createPickersReady } from '../../shared/utils/pickers-ready'; - Closing Date + {{ + 'ACCOUNTING_CLOSURES.CLOSING_DATE' | appTranslate + }} @if (pickersReady()) { } diff --git a/src/app/features/accounting/accounting-rule-form.component.test.ts b/src/app/features/accounting/accounting-rule-form.component.test.ts new file mode 100644 index 000000000..0a800be5a --- /dev/null +++ b/src/app/features/accounting/accounting-rule-form.component.test.ts @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { provideRouter } from '@angular/router'; +import { provideTranslateTesting } from '../../testing/i18n-testing'; +import { expectLookedUp } from '../../testing/translated-text'; +import { AccountingRuleFormComponent } from './accounting-rule-form.component'; + +describe('AccountingRuleFormComponent', () => { + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AccountingRuleFormComponent], + providers: [ + ...provideTranslateTesting(), + provideNoopAnimations(), + provideHttpClient(), + provideHttpClientTesting(), + provideRouter([]), + ], + }).compileComponents(); + + fixture = TestBed.createComponent(AccountingRuleFormComponent); + fixture.detectChanges(); + }); + + it('renders its headings and labels through the translation adapter', () => { + expectLookedUp(fixture.nativeElement, [ + 'ACCOUNTING_RULES.NAME', + 'ACCOUNTING_RULES.DEBIT_DETAILS', + 'ACCOUNTING_RULES.CREDIT_DETAILS', + 'ACCOUNTING_RULES.FIXED_ACCOUNT', + 'ACCOUNTING_RULES.ACCOUNT_TAGS', + 'ACCOUNTING_RULES.ACCOUNT_TO_DEBIT', + 'ACCOUNTING_RULES.ACCOUNT_TO_CREDIT', + 'ACCOUNTING_RULES.ALLOW_MULTIPLE_DEBIT_ENTRIES', + 'ACCOUNTING_RULES.ALLOW_MULTIPLE_CREDIT_ENTRIES', + ]); + }); + + it('labels the tag selectors once tags are chosen over a fixed account', () => { + // Choose "Account Tags" the way a user does, so the view is marked for check. + const groups = (fixture.nativeElement as HTMLElement).querySelectorAll('ion-radio-group'); + expect(groups).toHaveLength(2); + groups.forEach((group) => { + (group as HTMLElement & { value: string }).value = 'tags'; + group.dispatchEvent(new CustomEvent('ionChange')); + }); + fixture.detectChanges(); + + expectLookedUp(fixture.nativeElement, [ + 'ACCOUNTING_RULES.DEBIT_TAGS', + 'ACCOUNTING_RULES.CREDIT_TAGS', + ]); + }); +}); diff --git a/src/app/features/accounting/accounting-rule-form.component.ts b/src/app/features/accounting/accounting-rule-form.component.ts index d74111900..ca9ec795c 100644 --- a/src/app/features/accounting/accounting-rule-form.component.ts +++ b/src/app/features/accounting/accounting-rule-form.component.ts @@ -23,6 +23,7 @@ import { RouterModule, Router, ActivatedRoute } from '@angular/router'; import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Observable } from 'rxjs'; import { AccountingRulesService } from '../../api/api/accountingRules.service'; +import { TranslatePipe } from '../../core/adapters'; import { IonButton, IonCheckbox, @@ -42,6 +43,7 @@ import { imports: [ RouterModule, ReactiveFormsModule, + TranslatePipe, IonButton, IonInput, IonTextarea, @@ -59,8 +61,12 @@ import {
- Rule Name - + {{ 'ACCOUNTING_RULES.NAME' | appTranslate }} + @@ -75,17 +81,21 @@ import {
-

Debit Details

+

{{ 'ACCOUNTING_RULES.DEBIT_DETAILS' | appTranslate }}

- Fixed Account - Account Tags + {{ + 'ACCOUNTING_RULES.FIXED_ACCOUNT' | appTranslate + }} + {{ 'ACCOUNTING_RULES.ACCOUNT_TAGS' | appTranslate }} @if (ruleForm.get('debitRuleType')?.value === 'fixedAccount') { - Account to Debit + {{ + 'ACCOUNTING_RULES.ACCOUNT_TO_DEBIT' | appTranslate + }} @@ -100,9 +110,11 @@ import { @if (ruleForm.get('debitRuleType')?.value === 'tags') { - Debit Tags + {{ + 'ACCOUNTING_RULES.DEBIT_TAGS' | appTranslate + }} - Allow Multiple Debit Entries + {{ 'ACCOUNTING_RULES.ALLOW_MULTIPLE_DEBIT_ENTRIES' | appTranslate }}
-

Credit Details

+

{{ 'ACCOUNTING_RULES.CREDIT_DETAILS' | appTranslate }}

- Fixed Account - Account Tags + {{ + 'ACCOUNTING_RULES.FIXED_ACCOUNT' | appTranslate + }} + {{ 'ACCOUNTING_RULES.ACCOUNT_TAGS' | appTranslate }} @if (ruleForm.get('creditRuleType')?.value === 'fixedAccount') { - Account to Credit + {{ + 'ACCOUNTING_RULES.ACCOUNT_TO_CREDIT' | appTranslate + }} @@ -147,9 +163,11 @@ import { @if (ruleForm.get('creditRuleType')?.value === 'tags') { - Credit Tags + {{ + 'ACCOUNTING_RULES.CREDIT_TAGS' | appTranslate + }} - Allow Multiple Credit Entries + {{ 'ACCOUNTING_RULES.ALLOW_MULTIPLE_CREDIT_ENTRIES' | appTranslate }}
diff --git a/src/app/features/accounting/charges/charge-form.component.test.ts b/src/app/features/accounting/charges/charge-form.component.test.ts new file mode 100644 index 000000000..b5e0b364b --- /dev/null +++ b/src/app/features/accounting/charges/charge-form.component.test.ts @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { provideRouter } from '@angular/router'; +import { provideTranslateTesting } from '../../../testing/i18n-testing'; +import { expectLookedUp } from '../../../testing/translated-text'; +import { ChargeFormComponent } from './charge-form.component'; + +describe('ChargeFormComponent', () => { + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ChargeFormComponent], + providers: [ + ...provideTranslateTesting(), + provideNoopAnimations(), + provideHttpClient(), + provideHttpClientTesting(), + provideRouter([]), + ], + }).compileComponents(); + + fixture = TestBed.createComponent(ChargeFormComponent); + fixture.detectChanges(); + }); + + it('renders its field labels through the translation adapter', () => { + expectLookedUp(fixture.nativeElement, [ + 'CHARGES.APPLIES_TO', + 'CHARGES.TIME_TYPE', + 'CHARGES.CALCULATION_TYPE', + ]); + }); +}); diff --git a/src/app/features/accounting/charges/charge-form.component.ts b/src/app/features/accounting/charges/charge-form.component.ts index f0db248e9..ab0188e8a 100644 --- a/src/app/features/accounting/charges/charge-form.component.ts +++ b/src/app/features/accounting/charges/charge-form.component.ts @@ -96,9 +96,9 @@ import { - Applies To + {{ 'CHARGES.APPLIES_TO' | translate }} - Charge Time Type + {{ 'CHARGES.TIME_TYPE' | translate }} - Calculation Type + {{ + 'CHARGES.CALCULATION_TYPE' | translate + }} { + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FinancialActivityMappingFormComponent], + providers: [ + ...provideTranslateTesting(), + provideNoopAnimations(), + provideHttpClient(), + provideHttpClientTesting(), + provideRouter([]), + ], + }).compileComponents(); + + fixture = TestBed.createComponent(FinancialActivityMappingFormComponent); + fixture.detectChanges(); + }); + + it('renders its field labels through the translation adapter', () => { + expectLookedUp(fixture.nativeElement, [ + 'ACCOUNTING.FINANCIAL_ACTIVITY', + 'ACCOUNTING.GL_ACCOUNT', + ]); + }); +}); diff --git a/src/app/features/accounting/financial-activity-mapping-form.component.ts b/src/app/features/accounting/financial-activity-mapping-form.component.ts index faeea5659..c639219b0 100644 --- a/src/app/features/accounting/financial-activity-mapping-form.component.ts +++ b/src/app/features/accounting/financial-activity-mapping-form.component.ts @@ -23,6 +23,7 @@ import { RouterModule, Router, ActivatedRoute } from '@angular/router'; import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Observable } from 'rxjs'; import { MappingFinancialActivitiesToAccountsService } from '../../api/api/mappingFinancialActivitiesToAccounts.service'; +import { TranslatePipe } from '../../core/adapters'; import { IonButton, IonItem, @@ -37,6 +38,7 @@ import { imports: [ RouterModule, ReactiveFormsModule, + TranslatePipe, IonButton, IonItem, IonLabel, @@ -49,9 +51,11 @@ import { - Financial Activity + {{ + 'ACCOUNTING.FINANCIAL_ACTIVITY' | appTranslate + }} - GL Account + {{ 'ACCOUNTING.GL_ACCOUNT' | appTranslate }} { let component: JournalEntryFormComponent; @@ -119,4 +120,14 @@ describe('JournalEntryFormComponent', () => { }), ); }); + + it('renders its labels through the translation adapter', () => { + expectLookedUp(fixture.nativeElement, [ + 'JOURNAL_ENTRIES.CREATE', + 'JOURNAL_ENTRIES.TRANSACTION_DATE', + 'JOURNAL_ENTRIES.REFERENCE_NUMBER', + 'JOURNAL_ENTRIES.ADD_DEBIT', + 'JOURNAL_ENTRIES.ADD_CREDIT', + ]); + }); }); diff --git a/src/app/features/accounting/journal-entry-form.component.ts b/src/app/features/accounting/journal-entry-form.component.ts index b0e697a58..88087fbb6 100644 --- a/src/app/features/accounting/journal-entry-form.component.ts +++ b/src/app/features/accounting/journal-entry-form.component.ts @@ -90,7 +90,7 @@ import { createPickersReady } from '../../shared/utils/pickers-ready'; - Add Journal Entry + {{ 'JOURNAL_ENTRIES.CREATE' | translate }} @@ -134,7 +134,9 @@ import { createPickersReady } from '../../shared/utils/pickers-ready'; - Transaction Date + {{ + 'JOURNAL_ENTRIES.TRANSACTION_DATE' | translate + }} @if (pickersReady()) { } @@ -154,9 +156,11 @@ import { createPickersReady } from '../../shared/utils/pickers-ready'; - Reference Number + {{ + 'JOURNAL_ENTRIES.REFERENCE_NUMBER' | translate + }} @@ -206,7 +210,8 @@ import { createPickersReady } from '../../shared/utils/pickers-ready'; } - Add Debit + + {{ 'JOURNAL_ENTRIES.ADD_DEBIT' | translate }} @@ -253,7 +258,8 @@ import { createPickersReady } from '../../shared/utils/pickers-ready'; } - Add Credit + + {{ 'JOURNAL_ENTRIES.ADD_CREDIT' | translate }} diff --git a/src/app/features/campaigns/email-messages/email-messages.component.test.ts b/src/app/features/campaigns/email-messages/email-messages.component.test.ts new file mode 100644 index 000000000..fb899da0a --- /dev/null +++ b/src/app/features/campaigns/email-messages/email-messages.component.test.ts @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { NotificationService } from '../../../core/services/notification.service'; +import { createSpyObj } from '../../../testing/mocks'; +import { provideTranslateTesting } from '../../../testing/i18n-testing'; +import { expectLookedUp } from '../../../testing/translated-text'; +import { EMAIL_TAB, EmailMessagesComponent } from './email-messages.component'; + +describe('EmailMessagesComponent', () => { + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [EmailMessagesComponent], + providers: [ + ...provideTranslateTesting(), + provideNoopAnimations(), + provideHttpClient(), + provideHttpClientTesting(), + { + provide: NotificationService, + useValue: createSpyObj(['success', 'error', 'show']), + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(EmailMessagesComponent); + fixture.detectChanges(); + }); + + it('labels the configuration editor through the translation adapter', () => { + fixture.componentInstance.onTabChange(EMAIL_TAB.configuration); + fixture.detectChanges(); + + expectLookedUp(fixture.nativeElement, ['EMAIL_MESSAGES.CONFIGURATION_JSON']); + }); +}); diff --git a/src/app/features/campaigns/email-messages/email-messages.component.ts b/src/app/features/campaigns/email-messages/email-messages.component.ts index 0cadb48ba..bd058f98b 100644 --- a/src/app/features/campaigns/email-messages/email-messages.component.ts +++ b/src/app/features/campaigns/email-messages/email-messages.component.ts @@ -285,9 +285,11 @@ export type EmailTab = (typeof EMAIL_TAB)[keyof typeof EMAIL_TAB]; @if (activeTab() === TAB.configuration) {
- Configuration JSON + {{ + 'EMAIL_MESSAGES.CONFIGURATION_JSON' | translate + }} { + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SmsCampaignFormComponent], + providers: [ + ...provideTranslateTesting(), + provideNoopAnimations(), + provideHttpClient(), + provideHttpClientTesting(), + provideRouter([]), + { + provide: NotificationService, + useValue: createSpyObj(['success', 'error', 'show']), + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(SmsCampaignFormComponent); + fixture.detectChanges(); + }); + + it('renders the parameterized message label through the translation adapter', () => { + expectLookedUp(fixture.nativeElement, ['SMS_CAMPAIGNS.PARAMETERIZED_MESSAGE']); + }); +}); diff --git a/src/app/features/campaigns/sms-campaigns/sms-campaign-form.component.ts b/src/app/features/campaigns/sms-campaigns/sms-campaign-form.component.ts index c5706116e..71c970143 100644 --- a/src/app/features/campaigns/sms-campaigns/sms-campaign-form.component.ts +++ b/src/app/features/campaigns/sms-campaigns/sms-campaign-form.component.ts @@ -130,9 +130,11 @@ import { - Parameterized Message + {{ + 'SMS_CAMPAIGNS.PARAMETERIZED_MESSAGE' | translate + }} diff --git a/src/app/features/fintech/asset-owner-view/asset-owner-view.component.test.ts b/src/app/features/fintech/asset-owner-view/asset-owner-view.component.test.ts new file mode 100644 index 000000000..2f8acbdf5 --- /dev/null +++ b/src/app/features/fintech/asset-owner-view/asset-owner-view.component.test.ts @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { provideHttpClient } from '@angular/common/http'; +import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { provideRouter } from '@angular/router'; +import { provideTranslateTesting } from '../../../testing/i18n-testing'; +import { expectLookedUp } from '../../../testing/translated-text'; +import { AssetOwnerViewComponent } from './asset-owner-view.component'; + +describe('AssetOwnerViewComponent', () => { + let fixture: ComponentFixture; + let http: HttpTestingController; + + /** Answers every outstanding request whose URL ends with `suffix`, however many views asked. */ + function flushAll(suffix: string, body: object): void { + for (const request of http.match((r) => r.url.endsWith(suffix))) { + request.flush(body); + } + } + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AssetOwnerViewComponent], + providers: [ + ...provideTranslateTesting(), + provideNoopAnimations(), + provideHttpClient(), + provideHttpClientTesting(), + provideRouter([]), + ], + }).compileComponents(); + + http = TestBed.inject(HttpTestingController); + fixture = TestBed.createComponent(AssetOwnerViewComponent); + fixture.detectChanges(); + + // The screen renders nothing until the transfer arrives, and asks for its journal entries + // only once it has. + flushAll('/external-asset-owners/transfers', { + content: [{ transferId: 7, status: 'ACTIVE', owner: { externalId: 'OWNER-1' } }], + }); + fixture.detectChanges(); + flushAll('/transfers/7/journal-entries', { journalEntryData: { content: [] } }); + fixture.detectChanges(); + }); + + it('renders the loan link and the journal entries tab through the translation adapter', () => { + expectLookedUp(fixture.nativeElement, ['ASSET_OWNERS.VIEW_LOAN_ACCOUNT', 'nav.journalEntries']); + }); +}); diff --git a/src/app/features/fintech/asset-owner-view/asset-owner-view.component.ts b/src/app/features/fintech/asset-owner-view/asset-owner-view.component.ts index 5283b54f0..fdc05a752 100644 --- a/src/app/features/fintech/asset-owner-view/asset-owner-view.component.ts +++ b/src/app/features/fintech/asset-owner-view/asset-owner-view.component.ts @@ -95,7 +95,7 @@ export type AssetOwnerTab = (typeof ASSET_OWNER_TAB)[keyof typeof ASSET_OWNER_TA [routerLink]="['/loans/view', transfer.loan?.loanId]" > - View Loan Account + {{ 'ASSET_OWNERS.VIEW_LOAN_ACCOUNT' | translate }}
@@ -130,7 +130,7 @@ export type AssetOwnerTab = (typeof ASSET_OWNER_TAB)[keyof typeof ASSET_OWNER_TA
- Journal Entries + {{ 'nav.journalEntries' | translate }} {{ 'ASSET_OWNERS.LOAN_PRODUCT_ATTRIBUTES' | translate }} diff --git a/src/app/features/interop/interop-transfers.component.test.ts b/src/app/features/interop/interop-transfers.component.test.ts new file mode 100644 index 000000000..70107b365 --- /dev/null +++ b/src/app/features/interop/interop-transfers.component.test.ts @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { NotificationService } from '../../core/services/notification.service'; +import { createSpyObj } from '../../testing/mocks'; +import { provideTranslateTesting } from '../../testing/i18n-testing'; +import { expectLookedUp } from '../../testing/translated-text'; +import { InteropTransfersComponent } from './interop-transfers.component'; + +describe('InteropTransfersComponent', () => { + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [InteropTransfersComponent], + providers: [ + ...provideTranslateTesting(), + provideNoopAnimations(), + provideHttpClient(), + provideHttpClientTesting(), + { + provide: NotificationService, + useValue: createSpyObj(['success', 'error', 'show']), + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(InteropTransfersComponent); + fixture.detectChanges(); + }); + + it('renders the disburse and repay tab through the translation adapter', () => { + expectLookedUp(fixture.nativeElement, ['INTEROP.DISBURSE_REPAY']); + }); +}); diff --git a/src/app/features/interop/interop-transfers.component.ts b/src/app/features/interop/interop-transfers.component.ts index 45e9bca47..35a36e74f 100644 --- a/src/app/features/interop/interop-transfers.component.ts +++ b/src/app/features/interop/interop-transfers.component.ts @@ -96,7 +96,7 @@ export type InteropTab = (typeof INTEROP_TAB)[keyof typeof INTEROP_TAB]; {{ 'INTEROP.CREATE_TRANSFER' | translate }} - Disburse / Repay + {{ 'INTEROP.DISBURSE_REPAY' | translate }} diff --git a/src/app/features/loans/point-in-time/loans-point-in-time.component.test.ts b/src/app/features/loans/point-in-time/loans-point-in-time.component.test.ts new file mode 100644 index 000000000..090c501be --- /dev/null +++ b/src/app/features/loans/point-in-time/loans-point-in-time.component.test.ts @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { provideTranslateTesting } from '../../../testing/i18n-testing'; +import { expectLookedUp } from '../../../testing/translated-text'; +import { LoansPointInTimeComponent } from './loans-point-in-time.component'; + +describe('LoansPointInTimeComponent', () => { + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [LoansPointInTimeComponent], + providers: [ + ...provideTranslateTesting(), + provideNoopAnimations(), + provideHttpClient(), + provideHttpClientTesting(), + ], + }).compileComponents(); + + fixture = TestBed.createComponent(LoansPointInTimeComponent); + fixture.detectChanges(); + }); + + it('renders the results table headers through the translation adapter', () => { + fixture.componentInstance.results.set([{ id: 1, accountNo: '000000001' }]); + fixture.detectChanges(); + + expectLookedUp(fixture.nativeElement, [ + 'LOANS_POINT_IN_TIME.LOAN_ID', + 'COMMON.ACCOUNT_NO', + 'LOANS_POINT_IN_TIME.PRINCIPAL_DISBURSED', + 'LOANS_POINT_IN_TIME.PRINCIPAL_OUTSTANDING', + 'LOANS_POINT_IN_TIME.TOTAL_OUTSTANDING', + ]); + }); +}); diff --git a/src/app/features/loans/point-in-time/loans-point-in-time.component.ts b/src/app/features/loans/point-in-time/loans-point-in-time.component.ts index 9518b7530..08c6c0039 100644 --- a/src/app/features/loans/point-in-time/loans-point-in-time.component.ts +++ b/src/app/features/loans/point-in-time/loans-point-in-time.component.ts @@ -122,12 +122,14 @@ import { createPickersReady } from '../../../shared/utils/pickers-ready'; @if (!isLoading() && results().length > 0) { - + - + @@ -137,21 +139,27 @@ import { createPickersReady } from '../../../shared/utils/pickers-ready'; - + - + - + diff --git a/src/app/features/loans/rescheduling/reschedule-form.component.test.ts b/src/app/features/loans/rescheduling/reschedule-form.component.test.ts new file mode 100644 index 000000000..3b098665d --- /dev/null +++ b/src/app/features/loans/rescheduling/reschedule-form.component.test.ts @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { provideHttpClient } from '@angular/common/http'; +import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { ActivatedRoute, convertToParamMap, provideRouter } from '@angular/router'; +import { of } from 'rxjs'; +import { provideTranslateTesting } from '../../../testing/i18n-testing'; +import { expectLookedUp } from '../../../testing/translated-text'; +import { RescheduleFormComponent } from './reschedule-form.component'; + +describe('RescheduleFormComponent', () => { + let fixture: ComponentFixture; + let http: HttpTestingController; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RescheduleFormComponent], + providers: [ + ...provideTranslateTesting(), + provideNoopAnimations(), + provideHttpClient(), + provideHttpClientTesting(), + provideRouter([]), + { provide: ActivatedRoute, useValue: { paramMap: of(convertToParamMap({ loanId: '1' })) } }, + ], + }).compileComponents(); + + http = TestBed.inject(HttpTestingController); + fixture = TestBed.createComponent(RescheduleFormComponent); + fixture.detectChanges(); + + // No reasons on offer, so the form falls back to the manual reason field. + http.expectOne((r) => r.url.endsWith('/rescheduleloans/template')).flush({}); + http.expectOne((r) => r.url.endsWith('/loans/1')).flush({ repaymentSchedule: { periods: [] } }); + fixture.detectChanges(); + }); + + it('renders its heading, labels and hint through the translation adapter', () => { + expectLookedUp(fixture.nativeElement, [ + 'LOANS.RESCHEDULE_FORM.TITLE', + 'LOANS.RESCHEDULE_FORM.FROM_DATE', + 'LOANS.RESCHEDULE_FORM.FROM_DATE_HINT', + 'LOANS.RESCHEDULE_FORM.REASON_NAME_MANUAL', + 'COMMON.SUBMITTED_ON_DATE', + 'LOANS.RESCHEDULE_FORM.ADJUSTED_DUE_DATE', + 'LOANS.RESCHEDULE_FORM.GRACE_ON_PRINCIPAL', + 'LOANS.RESCHEDULE_FORM.GRACE_ON_INTEREST', + 'LOANS.RESCHEDULE_FORM.EXTRA_TERMS', + 'LOANS.RESCHEDULE_FORM.NEW_INTEREST_RATE', + ]); + }); +}); diff --git a/src/app/features/loans/rescheduling/reschedule-form.component.ts b/src/app/features/loans/rescheduling/reschedule-form.component.ts index 1f10c7dac..9ddff3561 100644 --- a/src/app/features/loans/rescheduling/reschedule-form.component.ts +++ b/src/app/features/loans/rescheduling/reschedule-form.component.ts @@ -84,7 +84,7 @@ import { createPickersReady } from '../../../shared/utils/pickers-ready';
- Request Loan Reschedule + {{ 'LOANS.RESCHEDULE_FORM.TITLE' | translate }} @@ -92,9 +92,11 @@ import { createPickersReady } from '../../../shared/utils/pickers-ready';
- Reschedule From Date + {{ + 'LOANS.RESCHEDULE_FORM.FROM_DATE' | translate + }} } - Must be an existing installment date + {{ 'LOANS.RESCHEDULE_FORM.FROM_DATE_HINT' | translate }}
@if (isAddingCustomReason()) { - Reason Name (Manual) + {{ + 'LOANS.RESCHEDULE_FORM.REASON_NAME_MANUAL' | translate + }} - Submitted On Date + {{ + 'COMMON.SUBMITTED_ON_DATE' | translate + }} @if (pickersReady()) { } @@ -178,7 +184,9 @@ import { createPickersReady } from '../../../shared/utils/pickers-ready'; - Adjusted Due Date (Optional) + {{ + 'LOANS.RESCHEDULE_FORM.ADJUSTED_DUE_DATE' | translate + }} @if (pickersReady()) { } @@ -208,9 +216,11 @@ import { createPickersReady } from '../../../shared/utils/pickers-ready'; - Grace on Principal + {{ + 'LOANS.RESCHEDULE_FORM.GRACE_ON_PRINCIPAL' | translate + }} - Grace on Interest + {{ + 'LOANS.RESCHEDULE_FORM.GRACE_ON_INTEREST' | translate + }} - Extra Terms + {{ + 'LOANS.RESCHEDULE_FORM.EXTRA_TERMS' | translate + }} - New Interest Rate + {{ + 'LOANS.RESCHEDULE_FORM.NEW_INTEREST_RATE' | translate + }} { let component: SavingsAccountViewComponent; @@ -116,6 +117,21 @@ describe('SavingsAccountViewComponent', () => { expect(component.account()?.savingsProductName).toBe('Regular Savings'); }); + it('renders the overview labels through the translation adapter', () => { + expectLookedUp(fixture.nativeElement, [ + 'SAVINGS.INTEREST_SETTINGS', + 'SAVINGS.NOMINAL_ANNUAL_INTEREST_RATE', + 'SAVINGS.COMPOUNDING_PERIOD', + 'SAVINGS.POSTING_PERIOD', + 'SAVINGS.INTEREST_CALC_DAYS_IN_YEAR', + 'SAVINGS.TIMELINE_AND_BALANCE', + 'COMMON.SUBMITTED_ON_DATE', + 'SAVINGS.ACTIVATED_ON_DATE', + 'SAVINGS.FIELD_OFFICER', + 'SAVINGS.BALANCE', + ]); + }); + /** * Fineract records a block in `subStatus` and leaves `status` as `Active`, so the status badge * cannot distinguish a frozen account from a healthy one. It also rejects each reversal unless diff --git a/src/app/features/products/savings-account-view.component.ts b/src/app/features/products/savings-account-view.component.ts index 0c9e1b2df..8a12cd449 100644 --- a/src/app/features/products/savings-account-view.component.ts +++ b/src/app/features/products/savings-account-view.component.ts @@ -420,24 +420,28 @@ export type SavingsTab = (typeof SAVINGS_TAB)[keyof typeof SAVINGS_TAB]; - Interest Settings + {{ 'SAVINGS.INTEREST_SETTINGS' | appTranslate }}
- Nominal Annual Interest Rate + {{ + 'SAVINGS.NOMINAL_ANNUAL_INTEREST_RATE' | appTranslate + }} {{ account()?.nominalAnnualInterestRate }}%
- Compounding Period + {{ 'SAVINGS.COMPOUNDING_PERIOD' | appTranslate }} {{ account()?.interestCompoundingPeriodType?.value }}
- Posting Period + {{ 'SAVINGS.POSTING_PERIOD' | appTranslate }} {{ account()?.interestPostingPeriodType?.value }}
- Interest Calculation Day-in-Year + {{ + 'SAVINGS.INTEREST_CALC_DAYS_IN_YEAR' | appTranslate + }} {{ account()?.interestCalculationDaysInYearType?.value }} @@ -449,24 +453,24 @@ export type SavingsTab = (typeof SAVINGS_TAB)[keyof typeof SAVINGS_TAB]; - Timeline & Balance + {{ 'SAVINGS.TIMELINE_AND_BALANCE' | appTranslate }}
- Submitted On Date + {{ 'COMMON.SUBMITTED_ON_DATE' | appTranslate }} {{ formattedSubmittedDate }}
- Activated On Date + {{ 'SAVINGS.ACTIVATED_ON_DATE' | appTranslate }} {{ formattedActivatedDate }}
- Field Officer + {{ 'SAVINGS.FIELD_OFFICER' | appTranslate }} {{ account()?.fieldOfficerName || '-' }}
- Account Balance + {{ 'SAVINGS.BALANCE' | appTranslate }} {{ account()?.currency?.displaySymbol }} {{ account()?.summary?.accountBalance || 0 | number: '1.2-2' }} diff --git a/src/app/features/security/audit-logs/audit-logs-list.component.test.ts b/src/app/features/security/audit-logs/audit-logs-list.component.test.ts index 4d036b17b..69a7be9f0 100644 --- a/src/app/features/security/audit-logs/audit-logs-list.component.test.ts +++ b/src/app/features/security/audit-logs/audit-logs-list.component.test.ts @@ -23,6 +23,7 @@ import { AuditLogsListComponent } from './audit-logs-list.component'; import { AuditsService } from '../../../api'; import { of, Observable } from 'rxjs'; import { provideTranslateTesting } from '../../../testing/i18n-testing'; +import { expectLookedUp } from '../../../testing/translated-text'; import { provideNoopAnimations } from '@angular/platform-browser/animations'; import { PageEvent, SortEvent } from '../../../shared/models/table.model'; import { provideIonicTesting } from '../../../testing/ionic-testing'; @@ -94,6 +95,19 @@ describe('AuditLogsListComponent', () => { expect(component.auditLogs()[0]['entityName']).toBe('Client'); }); + it('renders the filter labels through the translation adapter', () => { + fixture.detectChanges(); + expectLookedUp(fixture.nativeElement, [ + 'SECURITY.ACTION_NAME', + 'SECURITY.ENTITY_NAME', + 'SECURITY.RESOURCE_ID', + 'SECURITY.MAKER_ID', + 'SECURITY.MAKER_DATE_FROM', + 'SECURITY.MAKER_DATE_TO', + 'SECURITY.PROCESSING_RESULT', + ]); + }); + it('should handle apply and reset filters', () => { fixture.detectChanges(); diff --git a/src/app/features/security/audit-logs/audit-logs-list.component.ts b/src/app/features/security/audit-logs/audit-logs-list.component.ts index c0f6165d2..de49a48a2 100644 --- a/src/app/features/security/audit-logs/audit-logs-list.component.ts +++ b/src/app/features/security/audit-logs/audit-logs-list.component.ts @@ -99,27 +99,33 @@ export interface AuditFilters {
- Action Name + {{ + 'SECURITY.ACTION_NAME' | appTranslate + }} - Entity Name + {{ + 'SECURITY.ENTITY_NAME' | appTranslate + }} - Resource ID + {{ + 'SECURITY.RESOURCE_ID' | appTranslate + }} - Maker ID + {{ 'SECURITY.MAKER_ID' | appTranslate }} - Maker Date From + {{ + 'SECURITY.MAKER_DATE_FROM' | appTranslate + }} @if (pickersReady()) { - Maker Date To + {{ + 'SECURITY.MAKER_DATE_TO' | appTranslate + }} @if (pickersReady()) { - Processing Result + {{ + 'SECURITY.PROCESSING_RESULT' | appTranslate + }} diff --git a/src/app/features/settings/working-days.component.test.ts b/src/app/features/settings/working-days.component.test.ts index 0f2ecb77a..0a7fd7f3d 100644 --- a/src/app/features/settings/working-days.component.test.ts +++ b/src/app/features/settings/working-days.component.test.ts @@ -29,6 +29,7 @@ import { } from '../../testing/i18n-testing'; import { provideNoopAnimations } from '@angular/platform-browser/animations'; import { NotificationService } from '../../core/services/notification.service'; +import { expectLookedUp } from '../../testing/translated-text'; describe('WorkingDaysComponent', () => { let component: WorkingDaysComponent; @@ -96,6 +97,15 @@ describe('WorkingDaysComponent', () => { expect(text).not.toContain('COMMON.MONDAY'); }); + it('renders its headings and rules through the translation adapter', () => { + expectLookedUp(fixture.nativeElement, [ + 'WORKING_DAYS.TITLE', + 'WORKING_DAYS.REPAYMENTS_RESCHEDULING_RULE', + 'WORKING_DAYS.EXTEND_TERM_DAILY_REPAYMENTS', + 'WORKING_DAYS.EXTEND_TERM_HOLIDAY_REPAYMENTS', + ]); + }); + it('should submit a WorkingDaysUpdateRequest on save', () => { workingDaysServiceSpy.putWorkingdays.mockReturnValue( of({}) as unknown as ReturnType, diff --git a/src/app/features/settings/working-days.component.ts b/src/app/features/settings/working-days.component.ts index 98da63b34..3b9361a51 100644 --- a/src/app/features/settings/working-days.component.ts +++ b/src/app/features/settings/working-days.component.ts @@ -62,7 +62,7 @@ import {
- Working Days & Reschedule Rules + {{ 'WORKING_DAYS.TITLE' | appTranslate }} @@ -80,9 +80,11 @@ import {
- Repayments Rescheduling Rule + {{ + 'WORKING_DAYS.REPAYMENTS_RESCHEDULING_RULE' | appTranslate + }} - Extend Term for Daily Repayments + {{ 'WORKING_DAYS.EXTEND_TERM_DAILY_REPAYMENTS' | appTranslate }} - Extend Term for Repayments on Holidays + {{ 'WORKING_DAYS.EXTEND_TERM_HOLIDAY_REPAYMENTS' | appTranslate }}
diff --git a/src/app/features/transfers/standing-instruction-form.component.test.ts b/src/app/features/transfers/standing-instruction-form.component.test.ts index 80749de04..2cb760f83 100644 --- a/src/app/features/transfers/standing-instruction-form.component.test.ts +++ b/src/app/features/transfers/standing-instruction-form.component.test.ts @@ -25,6 +25,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { of, Observable } from 'rxjs'; import { provideTranslateTesting } from '../../testing/i18n-testing'; import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { expectLookedUp } from '../../testing/translated-text'; describe('StandingInstructionFormComponent', () => { let component: StandingInstructionFormComponent; @@ -95,6 +96,13 @@ describe('StandingInstructionFormComponent', () => { expect(component.isEditMode).toBe(false); }); + it('renders the transfer type options through the translation adapter', () => { + expectLookedUp(fixture.nativeElement, [ + 'CLIENTS.TRANSFER_TYPE_ACCOUNT_TRANSFER', + 'CLIENTS.TRANSFER_TYPE_LOAN_REPAYMENT', + ]); + }); + it('should load loan accounts when account type changes to loan', () => { component.request.fromClientId = '10'; component.request.fromAccountType = '1'; diff --git a/src/app/features/transfers/standing-instruction-form.component.ts b/src/app/features/transfers/standing-instruction-form.component.ts index ab096c426..5aef5a044 100644 --- a/src/app/features/transfers/standing-instruction-form.component.ts +++ b/src/app/features/transfers/standing-instruction-form.component.ts @@ -268,8 +268,12 @@ export interface MiniAccount { [(ngModel)]="request.transferType" required > - Account Transfer - Loan Repayment + {{ + 'CLIENTS.TRANSFER_TYPE_ACCOUNT_TRANSFER' | translate + }} + {{ + 'CLIENTS.TRANSFER_TYPE_LOAN_REPAYMENT' | translate + }} diff --git a/src/app/testing/translated-text.ts b/src/app/testing/translated-text.ts new file mode 100644 index 000000000..0c1118abf --- /dev/null +++ b/src/app/testing/translated-text.ts @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import en from '../../assets/i18n/en.json'; + +/** Follows a dotted translation key down the English catalogue. */ +function englishFor(key: string): unknown { + return key + .split('.') + .reduce( + (node, part) => + node !== null && typeof node === 'object' + ? (node as Record)[part] + : undefined, + en, + ); +} + +/** + * The text each element holds directly, with whitespace collapsed. + * + * Direct text only, so ` Add Debit` yields "Add Debit" and a + * heading that merely *contains* the phrase inside a longer sentence does not. A literal written + * into a template is always a text node of its own element; a phrase that happens to be part of + * other copy is not the defect this looks for. + */ +function typedText(root: Element): Set { + const found = new Set(); + const elements = [root, ...Array.from(root.querySelectorAll('*'))]; + for (const element of elements) { + const own = Array.from(element.childNodes) + .filter((child) => child.nodeType === Node.TEXT_NODE) + .map((child) => child.textContent ?? '') + .join(' ') + .replaceAll(/\s+/g, ' ') + .trim(); + if (own) found.add(own); + } + return found; +} + +/** + * Proves that text on screen was looked up rather than typed into the template. + * + * A heading written as `

Debit Details

` renders correctly in English and stays English in + * every other language, and nothing reports it: no key is referenced, so no key can be missing. + * The only place it shows is a render in which the catalogue is not the English one. A spec that + * leaves the translation library unconfigured, or binds the fake adapter, gets exactly that — + * every lookup echoes its key — so a looked-up label appears as its key and a typed one appears + * as English. + * + * For each key this asserts that + * + * 1. the key is defined in `en.json`, so the spec cannot pass on a key that would render blank; + * 2. the key was rendered, so the template really asks for it; and + * 3. the English the key stands for was not typed as an element's own text, which is what a + * hard-coded literal looks like. + * + * Reading the English from `en.json` rather than restating it in the spec keeps the two from + * drifting: rewording a string cannot turn this into a test of the old wording. + */ +export function expectLookedUp(root: Element, keys: readonly string[]): void { + const text = root.textContent ?? ''; + const typed = typedText(root); + for (const key of keys) { + const english = englishFor(key); + expect(english, `${key} is not defined in en.json`).toEqual(expect.any(String)); + expect(text, `${key} was never rendered`).toContain(key); + expect( + typed.has(english as string), + `"${english as string}" is typed into the template; use ${key}`, + ).toBe(false); + } +} diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 2a13a63b9..6618d4863 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -275,6 +275,7 @@ "SUBMIT": "Submit", "SUBMITTED_BY": "Submitted By", "SUBMITTED_ON": "Submitted On", + "SUBMITTED_ON_DATE": "Submitted On Date", "SUCCESS": "Done.", "SUNDAY": "Sunday", "TERM": "Term", @@ -498,7 +499,10 @@ }, "CHARGES": { "CREATE": "Create Charge", - "EDIT": "Edit Charge" + "EDIT": "Edit Charge", + "APPLIES_TO": "Applies To", + "TIME_TYPE": "Charge Time Type", + "CALCULATION_TYPE": "Calculation Type" }, "LOANS": { "ACCOUNT_NO": "Account No", @@ -741,7 +745,18 @@ "OWNER": "Owner", "PURCHASE_PRICE_RATIO": "Purchase Price Ratio", "SETTLEMENT_DATE": "Settlement Date", - "EFFECTIVE_FROM": "Effective From" + "EFFECTIVE_FROM": "Effective From", + "RESCHEDULE_FORM": { + "TITLE": "Request Loan Reschedule", + "FROM_DATE": "Reschedule From Date", + "FROM_DATE_HINT": "Must be an existing installment date", + "REASON_NAME_MANUAL": "Reason Name (Manual)", + "ADJUSTED_DUE_DATE": "Adjusted Due Date (Optional)", + "GRACE_ON_PRINCIPAL": "Grace on Principal", + "GRACE_ON_INTEREST": "Grace on Interest", + "EXTRA_TERMS": "Extra Terms", + "NEW_INTEREST_RATE": "New Interest Rate" + } }, "TEMPLATES": { "TITLE": "Document Templates", @@ -909,7 +924,9 @@ "BACK_TO_CLIENTS": "Back to clients", "ERRORS": { "NOT_FOUND": "This client doesn't exist, or you don't have permission to view it." - } + }, + "TRANSFER_TYPE_ACCOUNT_TRANSFER": "Account Transfer", + "TRANSFER_TYPE_LOAN_REPAYMENT": "Loan Repayment" }, "SAVINGS": { "CREATE_ACCOUNT": "Create Savings Account", @@ -976,7 +993,15 @@ "TRANSFER_IN": "Into this account", "TRANSFER_OUT": "Out of this account", "ACCOUNT_DETAILS": "Savings Account Details", - "TRANSACTION": "Savings Transaction" + "TRANSACTION": "Savings Transaction", + "INTEREST_SETTINGS": "Interest Settings", + "NOMINAL_ANNUAL_INTEREST_RATE": "Nominal Annual Interest Rate", + "COMPOUNDING_PERIOD": "Compounding Period", + "POSTING_PERIOD": "Posting Period", + "INTEREST_CALC_DAYS_IN_YEAR": "Interest Calculation Day-in-Year", + "TIMELINE_AND_BALANCE": "Timeline & Balance", + "ACTIVATED_ON_DATE": "Activated On Date", + "FIELD_OFFICER": "Field Officer" }, "USERS": { "CREATE": "Create User", @@ -1020,7 +1045,13 @@ }, "SECURITY": { "AUDIT_LOGS": "Audit Logs", - "RESOURCE_ID": "Resource ID" + "RESOURCE_ID": "Resource ID", + "ACTION_NAME": "Action Name", + "ENTITY_NAME": "Entity Name", + "MAKER_ID": "Maker ID", + "MAKER_DATE_FROM": "Maker Date From", + "MAKER_DATE_TO": "Maker Date To", + "PROCESSING_RESULT": "Processing Result" }, "SYSTEM": { "DATA_TABLES": "Data Tables", @@ -1084,7 +1115,17 @@ "NAME": "Rule Name", "OFFICE": "Office", "DEBITS": "Debit Accounts", - "CREDITS": "Credit Accounts" + "CREDITS": "Credit Accounts", + "DEBIT_DETAILS": "Debit Details", + "CREDIT_DETAILS": "Credit Details", + "FIXED_ACCOUNT": "Fixed Account", + "ACCOUNT_TAGS": "Account Tags", + "ACCOUNT_TO_DEBIT": "Account to Debit", + "DEBIT_TAGS": "Debit Tags", + "ALLOW_MULTIPLE_DEBIT_ENTRIES": "Allow Multiple Debit Entries", + "ACCOUNT_TO_CREDIT": "Account to Credit", + "CREDIT_TAGS": "Credit Tags", + "ALLOW_MULTIPLE_CREDIT_ENTRIES": "Allow Multiple Credit Entries" }, "ROLES": { "CREATE": "Create Role", @@ -1336,12 +1377,19 @@ "DELETE_TITLE": "Delete Holiday", "DELETE_CONFIRM": "Delete the holiday “{{name}}”? Repayment dates it moved return to their original schedule." }, + "WORKING_DAYS": { + "TITLE": "Working Days & Reschedule Rules", + "REPAYMENTS_RESCHEDULING_RULE": "Repayments Rescheduling Rule", + "EXTEND_TERM_DAILY_REPAYMENTS": "Extend Term for Daily Repayments", + "EXTEND_TERM_HOLIDAY_REPAYMENTS": "Extend Term for Repayments on Holidays" + }, "ACCOUNTING_CLOSURES": { "CREATE": "Close Period", "REOPEN": "Re-open Period", "CLOSING_DATE": "Closing Date", "IS_CLOSED": "Is Closed", - "COMMENTS": "Comments" + "COMMENTS": "Comments", + "CLOSE_ACCOUNTING_PERIOD": "Close Accounting Period" }, "JOURNAL_ENTRIES": { "CREATE": "Add Journal Entry", @@ -1373,7 +1421,9 @@ "ALL_ACCOUNTS": "All Accounts", "ALL_ENTRIES": "All Entries", "MANUAL_ENTRIES": "Manual Entries Only", - "LEDGER_ACCOUNT": "Ledger Account" + "LEDGER_ACCOUNT": "Ledger Account", + "ADD_DEBIT": "Add Debit", + "ADD_CREDIT": "Add Credit" }, "OFFICES": { "CREATE_OFFICE": "Create Office", @@ -1505,7 +1555,8 @@ "HEADER": "Header", "DETAIL": "Detail", "ACCOUNT_NAME": "Account Name", - "FINANCIAL_ACTIVITY": "Financial Activity" + "FINANCIAL_ACTIVITY": "Financial Activity", + "GL_ACCOUNT": "GL Account" }, "MODULES": { "CLIENTS_CONTRACTS": "Clients & Contracts", @@ -1903,7 +1954,11 @@ "DATE": "As of Date", "LOAN_IDS": "Loan IDs (comma-separated, optional)", "SEARCH": "Search", - "NO_RESULTS": "No results found" + "NO_RESULTS": "No results found", + "LOAN_ID": "Loan ID", + "PRINCIPAL_DISBURSED": "Principal Disbursed", + "PRINCIPAL_OUTSTANDING": "Principal Outstanding", + "TOTAL_OUTSTANDING": "Total Outstanding" }, "NOTIFICATIONS": { "TITLE": "Notifications", @@ -2545,7 +2600,8 @@ "ATTRIBUTE_KEY": "Key", "ATTRIBUTE_VALUE": "Value", "OWNER_ID": "Owner ID", - "PURCHASE_RATIO": "Purchase Ratio" + "PURCHASE_RATIO": "Purchase Ratio", + "VIEW_LOAN_ACCOUNT": "View Loan Account" }, "SPM": { "LOOKUP_TABLES": "Lookup Tables", @@ -2606,7 +2662,8 @@ "DELETE": "Delete", "SAVE": "Save", "SUCCESS": "Operation successful.", - "NO_DATA": "No data found." + "NO_DATA": "No data found.", + "PARAMETERIZED_MESSAGE": "Parameterized Message" }, "EMAIL_MESSAGES": { "TITLE": "Email Messages", @@ -2622,7 +2679,8 @@ "SAVE_CONFIG": "Save Config", "DELETE": "Delete", "SUCCESS": "Operation successful.", - "SENT_DATE": "Sent Date" + "SENT_DATE": "Sent Date", + "CONFIGURATION_JSON": "Configuration JSON" }, "OFFICE_TRANSACTIONS": { "TITLE": "Office Transactions", @@ -2763,7 +2821,8 @@ "TX_CODE": "Transaction Code", "QUOTE_CODE": "Quote Code", "CREATE_QUOTE": "Create Quote", - "QUOTE_BODY": "Quote Request (JSON)" + "QUOTE_BODY": "Quote Request (JSON)", + "DISBURSE_REPAY": "Disburse / Repay" }, "FORGOT_PASSWORD": { "TITLE": "Forgot Password",
Loan ID + {{ 'LOANS_POINT_IN_TIME.LOAN_ID' | translate }} + {{ row.id }} Account No{{ 'COMMON.ACCOUNT_NO' | translate }} {{ row.accountNo }} Principal Disbursed + {{ 'LOANS_POINT_IN_TIME.PRINCIPAL_DISBURSED' | translate }} + {{ row.principal?.principalDisbursed | number: '1.2-2' }} Principal Outstanding + {{ 'LOANS_POINT_IN_TIME.PRINCIPAL_OUTSTANDING' | translate }} + {{ row.principal?.principalOutstanding | number: '1.2-2' }} Total Outstanding + {{ 'LOANS_POINT_IN_TIME.TOTAL_OUTSTANDING' | translate }} + {{ row.total?.totalOutstanding | number: '1.2-2' }}