@cocoar/localization 0.1.0-beta.126 → 0.1.0-beta.151
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.
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { signal, Injectable, InjectionToken, inject,
|
|
3
|
-
import { of,
|
|
2
|
+
import { signal, computed, Injectable, InjectionToken, inject, DestroyRef, makeEnvironmentProviders, APP_INITIALIZER, Pipe, ChangeDetectorRef } from '@angular/core';
|
|
3
|
+
import { of, switchMap, tap, catchError, forkJoin, combineLatest, BehaviorSubject, Subject, lastValueFrom, map as map$1, distinctUntilChanged as distinctUntilChanged$1 } from 'rxjs';
|
|
4
4
|
import { ReadonlyState, BehaviorSubjectProxy } from '@cocoar/ts-utils';
|
|
5
5
|
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
|
6
6
|
import { HttpClient } from '@angular/common/http';
|
|
7
|
+
import { map, distinctUntilChanged } from 'rxjs/operators';
|
|
7
8
|
import { Temporal } from '@js-temporal/polyfill';
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -12,6 +13,14 @@ import { Temporal } from '@js-temporal/polyfill';
|
|
|
12
13
|
*/
|
|
13
14
|
class CoarLocalizationDataStore {
|
|
14
15
|
store = signal(new Map(), ...(ngDevMode ? [{ debugName: "store" }] : []));
|
|
16
|
+
/**
|
|
17
|
+
* Reactive signal that changes whenever any locale data is updated.
|
|
18
|
+
* Use this in computed() to ensure reactivity to data loading.
|
|
19
|
+
*/
|
|
20
|
+
dataVersion = computed(() => {
|
|
21
|
+
// Reading store() creates the signal dependency
|
|
22
|
+
return this.store().size;
|
|
23
|
+
}, ...(ngDevMode ? [{ debugName: "dataVersion" }] : []));
|
|
15
24
|
/**
|
|
16
25
|
* Set locale data for a specific locale.
|
|
17
26
|
* @param locale Locale code (e.g., 'en', 'de', 'en-US')
|
|
@@ -370,6 +379,12 @@ function coarInterpolate(template, params) {
|
|
|
370
379
|
});
|
|
371
380
|
}
|
|
372
381
|
|
|
382
|
+
/**
|
|
383
|
+
* Injection token for translation loaders (multi-provider).
|
|
384
|
+
* Loaders are executed in order and results are deep-merged.
|
|
385
|
+
* Intl loader is always first (provides common defaults from browser Intl API).
|
|
386
|
+
*/
|
|
387
|
+
const COAR_TRANSLATION_LOADERS = new InjectionToken('COAR_TRANSLATION_LOADERS');
|
|
373
388
|
/**
|
|
374
389
|
* Abstract loader for translation data.
|
|
375
390
|
*
|
|
@@ -697,11 +712,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
|
|
|
697
712
|
class CoarI18nService {
|
|
698
713
|
locale = inject(CoarLocalizationService);
|
|
699
714
|
store = inject(CoarTranslationStore);
|
|
700
|
-
|
|
715
|
+
loaders = inject(COAR_TRANSLATION_LOADERS, { optional: true }) ?? [];
|
|
701
716
|
destroyRef = inject(DestroyRef);
|
|
702
717
|
constructor() {
|
|
703
|
-
//
|
|
704
|
-
//
|
|
718
|
+
// Coordination: Wait for pending language changes, load translations, then resolve
|
|
719
|
+
// This ensures translations are ready BEFORE the language state changes
|
|
720
|
+
this.locale.pendingLanguageChange$
|
|
721
|
+
.pipe(takeUntilDestroyed(this.destroyRef), switchMap(({ language, resolve }) => {
|
|
722
|
+
// Load translations if not already loaded
|
|
723
|
+
if (this.store.hasLanguage(language)) {
|
|
724
|
+
resolve(); // Already loaded, resolve immediately
|
|
725
|
+
return of(void 0);
|
|
726
|
+
}
|
|
727
|
+
// Load and then resolve
|
|
728
|
+
return this.loadLanguage(language).pipe(tap(() => resolve()) // Signal that translations are ready
|
|
729
|
+
);
|
|
730
|
+
}))
|
|
731
|
+
.subscribe();
|
|
732
|
+
// Fallback: Auto-load translations when language changes without coordination
|
|
733
|
+
// (for backwards compatibility if someone directly mutates the subject)
|
|
705
734
|
this.locale.languageState.value$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((lang) => {
|
|
706
735
|
if (this.store.hasLanguage(lang)) {
|
|
707
736
|
return;
|
|
@@ -729,29 +758,56 @@ class CoarI18nService {
|
|
|
729
758
|
return coarInterpolate(value, params);
|
|
730
759
|
}
|
|
731
760
|
/**
|
|
732
|
-
* Loads translations
|
|
761
|
+
* Loads translations from all sources and deep-merges them.
|
|
733
762
|
*
|
|
734
|
-
*
|
|
735
|
-
*
|
|
763
|
+
* Sources are executed in order:
|
|
764
|
+
* 1. Intl (always first, provides common translations from browser APIs)
|
|
765
|
+
* 2. HTTP (if configured, merges application-specific overrides)
|
|
766
|
+
* 3. Custom sources (if provided, can add dynamic updates)
|
|
767
|
+
*
|
|
768
|
+
* Later sources override earlier sources at the key level.
|
|
736
769
|
*
|
|
737
770
|
* @param language - Language code to load
|
|
738
771
|
* @returns Observable that completes when loading finishes
|
|
739
772
|
*/
|
|
740
773
|
loadLanguage(language) {
|
|
741
|
-
// No
|
|
742
|
-
if (
|
|
774
|
+
// No loaders configured - mark as loaded with empty translations
|
|
775
|
+
if (this.loaders.length === 0) {
|
|
743
776
|
this.store.setTranslations(language, {});
|
|
744
777
|
return of(void 0);
|
|
745
778
|
}
|
|
746
|
-
|
|
747
|
-
|
|
779
|
+
// Load from all sources in parallel
|
|
780
|
+
const loadObservables = this.loaders.map((loader) => loader.loadTranslations(language).pipe(catchError((err) => {
|
|
781
|
+
console.warn(`[CoarI18n] Translation loader failed for '${language}':`, err);
|
|
782
|
+
return of(null); // Return null for failed sources
|
|
783
|
+
})));
|
|
784
|
+
return forkJoin(loadObservables).pipe(tap((results) => {
|
|
785
|
+
// Deep merge all sources (nulls are ignored)
|
|
786
|
+
const merged = this.mergeTranslations(results.filter((r) => r !== null));
|
|
787
|
+
this.store.setTranslations(language, merged);
|
|
748
788
|
}), switchMap(() => of(void 0)), catchError((error) => {
|
|
749
|
-
console.error(`Failed to load translations for
|
|
789
|
+
console.error(`[CoarI18n] Failed to load translations for '${language}':`, error);
|
|
750
790
|
// Store empty translations to prevent repeated load attempts
|
|
751
791
|
this.store.setTranslations(language, {});
|
|
752
792
|
return of(void 0);
|
|
753
793
|
}));
|
|
754
794
|
}
|
|
795
|
+
/**
|
|
796
|
+
* Merges multiple translation sources.
|
|
797
|
+
* Later sources override earlier sources at the key level.
|
|
798
|
+
*/
|
|
799
|
+
mergeTranslations(sources) {
|
|
800
|
+
const result = {};
|
|
801
|
+
for (const source of sources) {
|
|
802
|
+
if (!source)
|
|
803
|
+
continue;
|
|
804
|
+
// Merge all keys from this source
|
|
805
|
+
for (const [key, value] of Object.entries(source)) {
|
|
806
|
+
result[key] = value;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return result;
|
|
810
|
+
}
|
|
755
811
|
/**
|
|
756
812
|
* Preloads translations for a specific language.
|
|
757
813
|
*
|
|
@@ -776,6 +832,212 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
|
|
|
776
832
|
type: Injectable
|
|
777
833
|
}], ctorParameters: () => [] });
|
|
778
834
|
|
|
835
|
+
/**
|
|
836
|
+
* Translation loader that generates common translations from browser's Intl API.
|
|
837
|
+
*
|
|
838
|
+
* This loader provides default translations for common UI elements that can be
|
|
839
|
+
* automatically localized using browser APIs, without requiring JSON files.
|
|
840
|
+
*
|
|
841
|
+
* Translations generated:
|
|
842
|
+
* - Relative time labels (today, yesterday, tomorrow)
|
|
843
|
+
* - Month names (full and abbreviated)
|
|
844
|
+
* - Weekday names (full and abbreviated)
|
|
845
|
+
* - Common date/time units
|
|
846
|
+
*
|
|
847
|
+
* These translations can be overridden by registering additional loaders
|
|
848
|
+
* (e.g., HTTP loader) that provide custom values.
|
|
849
|
+
*
|
|
850
|
+
* @example
|
|
851
|
+
* ```ts
|
|
852
|
+
* // Automatic registration via provideCoarLocalization()
|
|
853
|
+
* provideCoarLocalization({ defaultLanguage: 'en' })
|
|
854
|
+
*
|
|
855
|
+
* // Intl loader provides: { 'common.today': 'today' }
|
|
856
|
+
* // HTTP loader can override: { 'common.today': 'Now' }
|
|
857
|
+
* ```
|
|
858
|
+
*/
|
|
859
|
+
class CoarIntlTranslationLoader extends CoarTranslationLoader {
|
|
860
|
+
loadTranslations(locale) {
|
|
861
|
+
return of(this.generateFromIntl(locale));
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Generate translations from browser Intl API.
|
|
865
|
+
*/
|
|
866
|
+
generateFromIntl(locale) {
|
|
867
|
+
const translations = {};
|
|
868
|
+
// Relative time labels (for date pickers, calendars, etc.)
|
|
869
|
+
try {
|
|
870
|
+
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
|
|
871
|
+
translations['common.today'] = rtf.format(0, 'day');
|
|
872
|
+
translations['common.yesterday'] = rtf.format(-1, 'day');
|
|
873
|
+
translations['common.tomorrow'] = rtf.format(1, 'day');
|
|
874
|
+
}
|
|
875
|
+
catch {
|
|
876
|
+
// Fallback for older browsers without RelativeTimeFormat
|
|
877
|
+
translations['common.today'] = 'today';
|
|
878
|
+
translations['common.yesterday'] = 'yesterday';
|
|
879
|
+
translations['common.tomorrow'] = 'tomorrow';
|
|
880
|
+
}
|
|
881
|
+
// Month names (full)
|
|
882
|
+
const monthFormatter = new Intl.DateTimeFormat(locale, { month: 'long' });
|
|
883
|
+
for (let m = 0; m < 12; m++) {
|
|
884
|
+
const date = new Date(2024, m, 1);
|
|
885
|
+
const monthName = monthFormatter.format(date);
|
|
886
|
+
translations[`common.month.${m + 1}`] = monthName;
|
|
887
|
+
}
|
|
888
|
+
// Month names (abbreviated)
|
|
889
|
+
const monthFormatterShort = new Intl.DateTimeFormat(locale, { month: 'short' });
|
|
890
|
+
for (let m = 0; m < 12; m++) {
|
|
891
|
+
const date = new Date(2024, m, 1);
|
|
892
|
+
const monthName = monthFormatterShort.format(date);
|
|
893
|
+
translations[`common.month.short.${m + 1}`] = monthName;
|
|
894
|
+
}
|
|
895
|
+
// Weekday names (full) - Monday=1, Sunday=7
|
|
896
|
+
const dayFormatter = new Intl.DateTimeFormat(locale, { weekday: 'long' });
|
|
897
|
+
for (let d = 0; d < 7; d++) {
|
|
898
|
+
// Jan 1, 2024 is Monday
|
|
899
|
+
const date = new Date(2024, 0, 1 + d);
|
|
900
|
+
const dayName = dayFormatter.format(date);
|
|
901
|
+
translations[`common.weekday.${d + 1}`] = dayName;
|
|
902
|
+
}
|
|
903
|
+
// Weekday names (abbreviated)
|
|
904
|
+
const dayFormatterShort = new Intl.DateTimeFormat(locale, { weekday: 'short' });
|
|
905
|
+
for (let d = 0; d < 7; d++) {
|
|
906
|
+
const date = new Date(2024, 0, 1 + d);
|
|
907
|
+
const dayName = dayFormatterShort.format(date);
|
|
908
|
+
translations[`common.weekday.short.${d + 1}`] = dayName;
|
|
909
|
+
}
|
|
910
|
+
return translations;
|
|
911
|
+
}
|
|
912
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarIntlTranslationLoader, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
913
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarIntlTranslationLoader });
|
|
914
|
+
}
|
|
915
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarIntlTranslationLoader, decorators: [{
|
|
916
|
+
type: Injectable
|
|
917
|
+
}] });
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* Built-in timezone provider using browser's Intl API.
|
|
921
|
+
*
|
|
922
|
+
* Always present as guaranteed baseline fallback.
|
|
923
|
+
* Returns the user's operating system timezone.
|
|
924
|
+
*
|
|
925
|
+
* Note: This is a one-time snapshot. If user changes OS timezone
|
|
926
|
+
* while app is running, this will not update (browser limitation).
|
|
927
|
+
*
|
|
928
|
+
* @internal
|
|
929
|
+
*/
|
|
930
|
+
class BrowserTimeZoneProvider {
|
|
931
|
+
timeZone$;
|
|
932
|
+
constructor() {
|
|
933
|
+
// Detect browser timezone using Intl API
|
|
934
|
+
// This is a one-time snapshot (browser doesn't provide reactive updates)
|
|
935
|
+
const browserTimeZone = this.detectBrowserTimeZone();
|
|
936
|
+
this.timeZone$ = of(browserTimeZone);
|
|
937
|
+
}
|
|
938
|
+
detectBrowserTimeZone() {
|
|
939
|
+
try {
|
|
940
|
+
// Use Intl API to get user's timezone
|
|
941
|
+
// Example return values: "America/New_York", "Europe/Paris", "Asia/Tokyo"
|
|
942
|
+
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
943
|
+
// Fallback to UTC if browser returns undefined (shouldn't happen in modern browsers)
|
|
944
|
+
return timeZone || 'UTC';
|
|
945
|
+
}
|
|
946
|
+
catch {
|
|
947
|
+
// Fallback to UTC if Intl API fails
|
|
948
|
+
return 'UTC';
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: BrowserTimeZoneProvider, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
952
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: BrowserTimeZoneProvider });
|
|
953
|
+
}
|
|
954
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: BrowserTimeZoneProvider, decorators: [{
|
|
955
|
+
type: Injectable
|
|
956
|
+
}], ctorParameters: () => [] });
|
|
957
|
+
|
|
958
|
+
/**
|
|
959
|
+
* Injection token for custom timezone providers (multi-provider).
|
|
960
|
+
*
|
|
961
|
+
* Providers are resolved in array order (first to last).
|
|
962
|
+
* First non-null value wins.
|
|
963
|
+
*
|
|
964
|
+
* Browser provider is always present as guaranteed baseline (automatically added).
|
|
965
|
+
*
|
|
966
|
+
* @internal
|
|
967
|
+
*/
|
|
968
|
+
const COAR_TIMEZONE_PROVIDERS = new InjectionToken('COAR_TIMEZONE_PROVIDERS');
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* Service for timezone resolution with pluggable provider hierarchy.
|
|
972
|
+
*
|
|
973
|
+
* Resolution order:
|
|
974
|
+
* 1. Custom providers (from config, in array order)
|
|
975
|
+
* 2. Browser provider (Intl API, always present as guaranteed baseline)
|
|
976
|
+
* 3. UTC safety net (if all providers return null)
|
|
977
|
+
*
|
|
978
|
+
* First non-null value wins. Changes propagate reactively.
|
|
979
|
+
*
|
|
980
|
+
* @example
|
|
981
|
+
* ```ts
|
|
982
|
+
* // In component
|
|
983
|
+
* export class MyComponent {
|
|
984
|
+
* private timeZoneService = inject(CoarTimeZoneService);
|
|
985
|
+
*
|
|
986
|
+
* // Reactive signal (updates when timezone changes)
|
|
987
|
+
* currentTimeZone = this.timeZoneService.currentTimeZone;
|
|
988
|
+
*
|
|
989
|
+
* // Observable stream
|
|
990
|
+
* timeZone$ = this.timeZoneService.timeZone$;
|
|
991
|
+
* }
|
|
992
|
+
* ```
|
|
993
|
+
*/
|
|
994
|
+
class CoarTimeZoneService {
|
|
995
|
+
browserProvider = inject(BrowserTimeZoneProvider);
|
|
996
|
+
customProviders = inject(COAR_TIMEZONE_PROVIDERS, { optional: true }) ?? [];
|
|
997
|
+
/**
|
|
998
|
+
* Observable stream of the current timezone (IANA identifier).
|
|
999
|
+
*
|
|
1000
|
+
* Emits when any provider in the hierarchy changes.
|
|
1001
|
+
* Uses `distinctUntilChanged()` to prevent unnecessary emissions.
|
|
1002
|
+
*
|
|
1003
|
+
* Resolution order: Custom providers → Browser → UTC
|
|
1004
|
+
*/
|
|
1005
|
+
timeZone$;
|
|
1006
|
+
/**
|
|
1007
|
+
* Signal of the current timezone (IANA identifier).
|
|
1008
|
+
*
|
|
1009
|
+
* Automatically updates when timezone changes.
|
|
1010
|
+
* Use this in templates or reactive contexts.
|
|
1011
|
+
*/
|
|
1012
|
+
currentTimeZone;
|
|
1013
|
+
constructor() {
|
|
1014
|
+
// Build provider chain: Custom providers → Browser (always last)
|
|
1015
|
+
const allProviders = [
|
|
1016
|
+
...this.customProviders,
|
|
1017
|
+
this.browserProvider, // Browser is guaranteed baseline, always present
|
|
1018
|
+
];
|
|
1019
|
+
// Combine all provider streams
|
|
1020
|
+
const providerStreams = allProviders.map((provider) => provider.timeZone$);
|
|
1021
|
+
this.timeZone$ = combineLatest(providerStreams).pipe(
|
|
1022
|
+
// Find first non-null value in priority order
|
|
1023
|
+
map((timeZones) => {
|
|
1024
|
+
const resolved = timeZones.find((tz) => tz !== null);
|
|
1025
|
+
// UTC as safety net if all providers return null (shouldn't happen)
|
|
1026
|
+
return resolved ?? 'UTC';
|
|
1027
|
+
}),
|
|
1028
|
+
// Prevent unnecessary emissions when value doesn't actually change
|
|
1029
|
+
distinctUntilChanged());
|
|
1030
|
+
// Initialize signal after timeZone$ is set up
|
|
1031
|
+
this.currentTimeZone = toSignal(this.timeZone$, { requireSync: true });
|
|
1032
|
+
}
|
|
1033
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarTimeZoneService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1034
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarTimeZoneService, providedIn: 'root' });
|
|
1035
|
+
}
|
|
1036
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarTimeZoneService, decorators: [{
|
|
1037
|
+
type: Injectable,
|
|
1038
|
+
args: [{ providedIn: 'root' }]
|
|
1039
|
+
}], ctorParameters: () => [] });
|
|
1040
|
+
|
|
779
1041
|
/**
|
|
780
1042
|
* Injection token for locale configuration.
|
|
781
1043
|
*/
|
|
@@ -824,6 +1086,16 @@ function provideCoarLocalization(config) {
|
|
|
824
1086
|
},
|
|
825
1087
|
CoarLocalizationService,
|
|
826
1088
|
CoarLocalizationDataStore,
|
|
1089
|
+
// TimeZone: Provide the timezone service
|
|
1090
|
+
CoarTimeZoneService,
|
|
1091
|
+
// TimeZone: Browser provider (always present as guaranteed baseline)
|
|
1092
|
+
BrowserTimeZoneProvider,
|
|
1093
|
+
// TimeZone: Register custom providers from config (if any)
|
|
1094
|
+
...(config.timeZoneProviders?.map((provider) => ({
|
|
1095
|
+
provide: COAR_TIMEZONE_PROVIDERS,
|
|
1096
|
+
multi: true,
|
|
1097
|
+
useValue: provider,
|
|
1098
|
+
})) ?? []),
|
|
827
1099
|
// L10n: Auto-include Intl source as first loader (provides complete defaults)
|
|
828
1100
|
{
|
|
829
1101
|
provide: COAR_LOCALIZATION_DATA_LOADERS,
|
|
@@ -835,6 +1107,12 @@ function provideCoarLocalization(config) {
|
|
|
835
1107
|
provide: COAR_I18N_PROVIDER,
|
|
836
1108
|
useClass: CoarI18nService,
|
|
837
1109
|
},
|
|
1110
|
+
// i18n: Auto-include Intl source as first translation loader (provides common defaults)
|
|
1111
|
+
{
|
|
1112
|
+
provide: COAR_TRANSLATION_LOADERS,
|
|
1113
|
+
multi: true,
|
|
1114
|
+
useClass: CoarIntlTranslationLoader,
|
|
1115
|
+
},
|
|
838
1116
|
// APP_INITIALIZER: Preload default language (L10n + i18n if loader registered)
|
|
839
1117
|
{
|
|
840
1118
|
provide: APP_INITIALIZER,
|
|
@@ -955,6 +1233,12 @@ class CoarLocalizationService {
|
|
|
955
1233
|
localeDataLoaders = inject(COAR_LOCALIZATION_DATA_LOADERS, { optional: true }) ?? [];
|
|
956
1234
|
defaultLanguage = this.config?.defaultLanguage ?? 'en';
|
|
957
1235
|
languageSubject = new BehaviorSubject(this.defaultLanguage);
|
|
1236
|
+
/**
|
|
1237
|
+
* Subject for coordinating language changes with i18n service.
|
|
1238
|
+
* The i18n service subscribes to this to load translations BEFORE the language change is emitted.
|
|
1239
|
+
* @internal
|
|
1240
|
+
*/
|
|
1241
|
+
pendingLanguageChange$ = new Subject();
|
|
958
1242
|
/**
|
|
959
1243
|
* Canonical language state.
|
|
960
1244
|
*
|
|
@@ -965,9 +1249,13 @@ class CoarLocalizationService {
|
|
|
965
1249
|
constructor() {
|
|
966
1250
|
// Expose to window for debugging
|
|
967
1251
|
if (typeof window !== 'undefined') {
|
|
968
|
-
window.__coarLocalizationStore =
|
|
969
|
-
this.localeDataStore;
|
|
1252
|
+
window.__coarLocalizationStore = this.localeDataStore;
|
|
970
1253
|
}
|
|
1254
|
+
// Load locale data for the default language on initialization
|
|
1255
|
+
// This ensures firstDayOfWeek and other formatting data is available immediately
|
|
1256
|
+
this.loadAndmergeLocalizationData(this.defaultLanguage).catch((error) => {
|
|
1257
|
+
console.warn(`[CoarLocalizationService] Failed to load initial locale data for '${this.defaultLanguage}':`, error);
|
|
1258
|
+
});
|
|
971
1259
|
}
|
|
972
1260
|
/**
|
|
973
1261
|
* Set the current language.
|
|
@@ -990,6 +1278,10 @@ class CoarLocalizationService {
|
|
|
990
1278
|
*/
|
|
991
1279
|
async setLanguage(language) {
|
|
992
1280
|
const current = this.languageState.value;
|
|
1281
|
+
// Skip if language hasn't changed
|
|
1282
|
+
if (current === language) {
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
993
1285
|
// Load locale data if not already loaded (cached)
|
|
994
1286
|
if (!this.localeDataStore.hasLocaleData(language)) {
|
|
995
1287
|
try {
|
|
@@ -1001,10 +1293,16 @@ class CoarLocalizationService {
|
|
|
1001
1293
|
// Formatting pipes will use fallback values
|
|
1002
1294
|
}
|
|
1003
1295
|
}
|
|
1004
|
-
//
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1296
|
+
// Wait for i18n service to load translations before emitting language change
|
|
1297
|
+
// Use a timeout in case i18n service is not injected (e.g., in tests)
|
|
1298
|
+
await Promise.race([
|
|
1299
|
+
new Promise((resolve) => {
|
|
1300
|
+
this.pendingLanguageChange$.next({ language, resolve });
|
|
1301
|
+
}),
|
|
1302
|
+
new Promise((resolve) => setTimeout(resolve, 100)), // Fallback timeout
|
|
1303
|
+
]);
|
|
1304
|
+
// Now emit the language change (UI will react with translations ready)
|
|
1305
|
+
this.languageSubject.next(language);
|
|
1008
1306
|
}
|
|
1009
1307
|
/**
|
|
1010
1308
|
* Load locale data from all sources and deep-merge them.
|
|
@@ -1425,7 +1723,7 @@ class CoarI18n {
|
|
|
1425
1723
|
*/
|
|
1426
1724
|
t$(key, params, fallback) {
|
|
1427
1725
|
// Re-evaluate on every language change from CoarLocalizationService
|
|
1428
|
-
return this.locale.languageState.value$.pipe(map(() => this.callT(key, params, fallback)), distinctUntilChanged());
|
|
1726
|
+
return this.locale.languageState.value$.pipe(map$1(() => this.callT(key, params, fallback)), distinctUntilChanged$1());
|
|
1429
1727
|
}
|
|
1430
1728
|
/**
|
|
1431
1729
|
* Signal variant that updates when the language changes.
|
|
@@ -1641,7 +1939,8 @@ function provideCoarI18nHttpSource(config) {
|
|
|
1641
1939
|
const headers = config?.headers;
|
|
1642
1940
|
return makeEnvironmentProviders([
|
|
1643
1941
|
{
|
|
1644
|
-
provide:
|
|
1942
|
+
provide: COAR_TRANSLATION_LOADERS,
|
|
1943
|
+
multi: true,
|
|
1645
1944
|
useFactory: () => {
|
|
1646
1945
|
const loader = new CoarHttpTranslationLoader();
|
|
1647
1946
|
loader.urlFn = urlFn;
|
|
@@ -1658,5 +1957,5 @@ function provideCoarI18nHttpSource(config) {
|
|
|
1658
1957
|
* Generated bundle index. Do not edit.
|
|
1659
1958
|
*/
|
|
1660
1959
|
|
|
1661
|
-
export { COAR_I18N_PROVIDER, COAR_LOCALIZATION_CONFIG, COAR_LOCALIZATION_DATA_LOADERS, CoarCurrencyPipe, CoarDatePipe, CoarHttpLocaleDataLoader, CoarHttpTranslationLoader, CoarI18n, CoarI18nContext, CoarI18nPipe, CoarI18nService, CoarIntlLocaleDataLoader, CoarLocalizationDataLoader, CoarLocalizationDataStore, CoarLocalizationService, CoarNumberPipe, CoarPercentPipe, CoarTranslationLoader, CoarTranslationStore, coarInterpolate, coarIsMissingTranslation, mergeLocalizationData, provideCoarI18nHttpSource, provideCoarL10nHttpSource, provideCoarLocalization };
|
|
1960
|
+
export { COAR_I18N_PROVIDER, COAR_LOCALIZATION_CONFIG, COAR_LOCALIZATION_DATA_LOADERS, COAR_TRANSLATION_LOADERS, CoarCurrencyPipe, CoarDatePipe, CoarHttpLocaleDataLoader, CoarHttpTranslationLoader, CoarI18n, CoarI18nContext, CoarI18nPipe, CoarI18nService, CoarIntlLocaleDataLoader, CoarIntlTranslationLoader, CoarLocalizationDataLoader, CoarLocalizationDataStore, CoarLocalizationService, CoarNumberPipe, CoarPercentPipe, CoarTimeZoneService, CoarTranslationLoader, CoarTranslationStore, coarInterpolate, coarIsMissingTranslation, mergeLocalizationData, provideCoarI18nHttpSource, provideCoarL10nHttpSource, provideCoarLocalization };
|
|
1662
1961
|
//# sourceMappingURL=cocoar-localization.mjs.map
|