Skip to content

Commit 9005166

Browse files
authored
feat: Add localeSignal() to UI and VaadinSession
Add a signal that reflects the current locale and updates automatically when setLocale() is called. This allows components to reactively respond to locale changes using the signals API.
1 parent fcd74ab commit 9005166

File tree

4 files changed

+318
-7
lines changed

4 files changed

+318
-7
lines changed

flow-server/src/main/java/com/vaadin/flow/component/UI.java

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@
8383
import com.vaadin.flow.server.auth.AnonymousAllowed;
8484
import com.vaadin.flow.server.communication.PushConnection;
8585
import com.vaadin.flow.shared.Registration;
86+
import com.vaadin.signals.WritableSignal;
87+
import com.vaadin.signals.local.ValueSignal;
8688

8789
/**
8890
* The topmost component in any component hierarchy. There is one UI for every
@@ -125,7 +127,8 @@ public class UI extends Component
125127

126128
private PushConfiguration pushConfiguration;
127129

128-
private Locale locale = Locale.getDefault();
130+
private final ValueSignal<Locale> localeSignal = new ValueSignal<>(
131+
Locale.getDefault());
129132

130133
private final UIInternals internals;
131134

@@ -806,7 +809,28 @@ Logger getLogger() {
806809
*/
807810
@Override
808811
public Locale getLocale() {
809-
return locale;
812+
return localeSignal.peek();
813+
}
814+
815+
/**
816+
* Gets a signal that holds the current locale of this UI.
817+
* <p>
818+
* The signal is the source of truth for the locale. Use
819+
* {@link WritableSignal#value()} to read the locale reactively (creates a
820+
* dependency when called inside a signal effect). Use {@link #getLocale()}
821+
* for non-reactive reads.
822+
* <p>
823+
* Note that writing directly to the signal will not notify
824+
* {@link com.vaadin.flow.i18n.LocaleChangeObserver LocaleChangeObserver}
825+
* instances. Use {@link #setLocale(Locale)} if you need observers to be
826+
* notified.
827+
*
828+
* @return a writable signal holding the current locale, never null
829+
* @see #setLocale(Locale)
830+
* @see #getLocale()
831+
*/
832+
public WritableSignal<Locale> localeSignal() {
833+
return localeSignal;
810834
}
811835

812836
/**
@@ -821,8 +845,8 @@ public Locale getLocale() {
821845
*/
822846
public void setLocale(Locale locale) {
823847
assert locale != null : "Null locale is not supported!";
824-
if (!this.locale.equals(locale)) {
825-
this.locale = locale;
848+
if (!getLocale().equals(locale)) {
849+
localeSignal.value(locale);
826850
EventUtil.informLocaleChangeObservers(this);
827851
}
828852
}

flow-server/src/main/java/com/vaadin/flow/server/VaadinSession.java

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@
5454
import com.vaadin.flow.server.startup.ApplicationConfiguration;
5555
import com.vaadin.flow.shared.Registration;
5656
import com.vaadin.flow.shared.communication.PushMode;
57+
import com.vaadin.signals.WritableSignal;
58+
import com.vaadin.signals.shared.SharedValueSignal;
5759

5860
/**
5961
* Contains everything that Vaadin needs to store for a specific user. This is
@@ -83,10 +85,14 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable {
8385
final List<SessionDestroyListener> destroyListeners = new CopyOnWriteArrayList<>();
8486

8587
/**
86-
* Default locale of the session.
88+
* Locale value used for serialization. The signal is the runtime source of
89+
* truth; this field is only used to persist the value across serialization.
8790
*/
8891
private Locale locale = Locale.getDefault();
8992

93+
private transient SharedValueSignal<Locale> localeSignal = new SharedValueSignal<>(
94+
locale);
95+
9096
/**
9197
* Session wide error handler which is used by default if an error is left
9298
* unhandled.
@@ -383,7 +389,28 @@ public DeploymentConfiguration getConfiguration() {
383389
*/
384390
public Locale getLocale() {
385391
checkHasLock();
386-
return locale;
392+
return localeSignal.peek();
393+
}
394+
395+
/**
396+
* Gets a signal that holds the current locale of this session.
397+
* <p>
398+
* The signal is the source of truth for the locale. Use
399+
* {@link WritableSignal#value()} to read the locale reactively (creates a
400+
* dependency when called inside a signal effect). Use {@link #getLocale()}
401+
* for non-reactive reads.
402+
* <p>
403+
* Note that writing directly to the signal will not propagate the locale
404+
* change to UIs in this session. Use {@link #setLocale(Locale)} if you need
405+
* the locale to be set on all UIs.
406+
*
407+
* @return a writable signal holding the current locale, never null
408+
* @see #setLocale(Locale)
409+
* @see #getLocale()
410+
*/
411+
public WritableSignal<Locale> localeSignal() {
412+
checkHasLock();
413+
return localeSignal;
387414
}
388415

389416
/**
@@ -399,7 +426,7 @@ public void setLocale(Locale locale) {
399426
assert locale != null : "Null locale is not supported!";
400427

401428
checkHasLock();
402-
this.locale = locale;
429+
localeSignal.value(locale);
403430

404431
getUIs().forEach(ui -> {
405432
Map<Class<?>, CurrentInstance> oldInstances = CurrentInstance
@@ -1093,6 +1120,7 @@ private void readObject(ObjectInputStream stream)
10931120
uIs = (Map<Integer, UI>) stream.readObject();
10941121
resourceRegistry = (StreamResourceRegistry) stream.readObject();
10951122
pendingAccessQueue = new ConcurrentLinkedQueue<>();
1123+
localeSignal = new SharedValueSignal<>(locale);
10961124
} finally {
10971125
CurrentInstance.clearAll();
10981126
CurrentInstance.restoreInstances(old);
@@ -1118,6 +1146,8 @@ private void writeObject(java.io.ObjectOutputStream stream)
11181146
}
11191147
}
11201148

1149+
// Sync locale field from signal for serialization
1150+
locale = localeSignal.value();
11211151
stream.defaultWriteObject();
11221152
if (serializeUIs) {
11231153
stream.writeObject(uIs);
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
* Copyright 2000-2026 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.vaadin.flow.component;
17+
18+
import java.util.Locale;
19+
20+
import org.junit.Test;
21+
22+
import com.vaadin.flow.dom.SignalsUnitTest;
23+
import com.vaadin.signals.WritableSignal;
24+
25+
import static org.junit.Assert.assertEquals;
26+
import static org.junit.Assert.assertNotNull;
27+
import static org.junit.Assert.assertSame;
28+
29+
/**
30+
* Unit tests for {@link UI#localeSignal()}.
31+
*/
32+
public class UILocaleSignalTest extends SignalsUnitTest {
33+
34+
@Test
35+
public void localeSignal_initialValue_matchesGetLocale() {
36+
UI ui = UI.getCurrent();
37+
WritableSignal<Locale> signal = ui.localeSignal();
38+
39+
assertNotNull("localeSignal() should never return null", signal);
40+
assertEquals("Signal value should match getLocale()", ui.getLocale(),
41+
signal.value());
42+
}
43+
44+
@Test
45+
public void localeSignal_setLocale_signalUpdated() {
46+
UI ui = UI.getCurrent();
47+
WritableSignal<Locale> signal = ui.localeSignal();
48+
49+
Locale initialLocale = ui.getLocale();
50+
Locale newLocale = Locale.FRENCH;
51+
52+
// Ensure we're actually changing the locale
53+
if (initialLocale.equals(newLocale)) {
54+
newLocale = Locale.GERMAN;
55+
}
56+
57+
ui.setLocale(newLocale);
58+
59+
assertEquals("Signal should reflect the new locale after setLocale()",
60+
newLocale, signal.value());
61+
assertEquals("getLocale() should also return the new locale", newLocale,
62+
ui.getLocale());
63+
}
64+
65+
@Test
66+
public void localeSignal_writeToSignal_updatesGetLocale() {
67+
UI ui = UI.getCurrent();
68+
WritableSignal<Locale> signal = ui.localeSignal();
69+
70+
Locale initialLocale = ui.getLocale();
71+
Locale newLocale = Locale.FRENCH;
72+
73+
// Ensure we're actually changing the locale
74+
if (initialLocale.equals(newLocale)) {
75+
newLocale = Locale.GERMAN;
76+
}
77+
78+
signal.value(newLocale);
79+
80+
assertEquals("getLocale() should reflect the new locale after "
81+
+ "writing to signal", newLocale, ui.getLocale());
82+
assertEquals("Signal should have the new value", newLocale,
83+
signal.value());
84+
}
85+
86+
@Test
87+
public void localeSignal_sameInstance_returnedOnMultipleCalls() {
88+
UI ui = UI.getCurrent();
89+
90+
WritableSignal<Locale> signal1 = ui.localeSignal();
91+
WritableSignal<Locale> signal2 = ui.localeSignal();
92+
93+
assertSame("localeSignal() should return the same instance on "
94+
+ "multiple calls", signal1, signal2);
95+
}
96+
97+
@Test
98+
public void localeSignal_multipleLocaleChanges_signalFollows() {
99+
UI ui = UI.getCurrent();
100+
WritableSignal<Locale> signal = ui.localeSignal();
101+
102+
ui.setLocale(Locale.FRENCH);
103+
assertEquals(Locale.FRENCH, signal.value());
104+
105+
ui.setLocale(Locale.GERMAN);
106+
assertEquals(Locale.GERMAN, signal.value());
107+
108+
ui.setLocale(Locale.JAPANESE);
109+
assertEquals(Locale.JAPANESE, signal.value());
110+
}
111+
112+
@Test
113+
public void localeSignal_multipleSignalWrites_getLocaleFollows() {
114+
UI ui = UI.getCurrent();
115+
WritableSignal<Locale> signal = ui.localeSignal();
116+
117+
signal.value(Locale.FRENCH);
118+
assertEquals(Locale.FRENCH, ui.getLocale());
119+
120+
signal.value(Locale.GERMAN);
121+
assertEquals(Locale.GERMAN, ui.getLocale());
122+
123+
signal.value(Locale.JAPANESE);
124+
assertEquals(Locale.JAPANESE, ui.getLocale());
125+
}
126+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
* Copyright 2000-2026 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.vaadin.flow.server;
17+
18+
import java.util.Locale;
19+
20+
import org.junit.Test;
21+
22+
import com.vaadin.flow.component.UI;
23+
import com.vaadin.flow.dom.SignalsUnitTest;
24+
import com.vaadin.signals.WritableSignal;
25+
26+
import static org.junit.Assert.assertEquals;
27+
import static org.junit.Assert.assertNotNull;
28+
import static org.junit.Assert.assertSame;
29+
30+
/**
31+
* Unit tests for {@link VaadinSession#localeSignal()}.
32+
*/
33+
public class VaadinSessionLocaleSignalTest extends SignalsUnitTest {
34+
35+
private VaadinSession getSession() {
36+
return UI.getCurrent().getSession();
37+
}
38+
39+
@Test
40+
public void localeSignal_initialValue_matchesGetLocale() {
41+
VaadinSession session = getSession();
42+
WritableSignal<Locale> signal = session.localeSignal();
43+
44+
assertNotNull("localeSignal() should never return null", signal);
45+
assertEquals("Signal value should match getLocale()",
46+
session.getLocale(), signal.value());
47+
}
48+
49+
@Test
50+
public void localeSignal_setLocale_signalUpdated() {
51+
VaadinSession session = getSession();
52+
WritableSignal<Locale> signal = session.localeSignal();
53+
54+
Locale initialLocale = session.getLocale();
55+
Locale newLocale = Locale.FRENCH;
56+
57+
// Ensure we're actually changing the locale
58+
if (initialLocale.equals(newLocale)) {
59+
newLocale = Locale.GERMAN;
60+
}
61+
62+
session.setLocale(newLocale);
63+
64+
assertEquals("Signal should reflect the new locale after setLocale()",
65+
newLocale, signal.value());
66+
assertEquals("getLocale() should also return the new locale", newLocale,
67+
session.getLocale());
68+
}
69+
70+
@Test
71+
public void localeSignal_writeToSignal_updatesGetLocale() {
72+
VaadinSession session = getSession();
73+
WritableSignal<Locale> signal = session.localeSignal();
74+
75+
Locale initialLocale = session.getLocale();
76+
Locale newLocale = Locale.FRENCH;
77+
78+
// Ensure we're actually changing the locale
79+
if (initialLocale.equals(newLocale)) {
80+
newLocale = Locale.GERMAN;
81+
}
82+
83+
signal.value(newLocale);
84+
85+
assertEquals("getLocale() should reflect the new locale after "
86+
+ "writing to signal", newLocale, session.getLocale());
87+
assertEquals("Signal should have the new value", newLocale,
88+
signal.value());
89+
}
90+
91+
@Test
92+
public void localeSignal_sameInstance_returnedOnMultipleCalls() {
93+
VaadinSession session = getSession();
94+
95+
WritableSignal<Locale> signal1 = session.localeSignal();
96+
WritableSignal<Locale> signal2 = session.localeSignal();
97+
98+
assertSame("localeSignal() should return the same instance on "
99+
+ "multiple calls", signal1, signal2);
100+
}
101+
102+
@Test
103+
public void localeSignal_multipleLocaleChanges_signalFollows() {
104+
VaadinSession session = getSession();
105+
WritableSignal<Locale> signal = session.localeSignal();
106+
107+
session.setLocale(Locale.FRENCH);
108+
assertEquals(Locale.FRENCH, signal.value());
109+
110+
session.setLocale(Locale.GERMAN);
111+
assertEquals(Locale.GERMAN, signal.value());
112+
113+
session.setLocale(Locale.JAPANESE);
114+
assertEquals(Locale.JAPANESE, signal.value());
115+
}
116+
117+
@Test
118+
public void localeSignal_multipleSignalWrites_getLocaleFollows() {
119+
VaadinSession session = getSession();
120+
WritableSignal<Locale> signal = session.localeSignal();
121+
122+
signal.value(Locale.FRENCH);
123+
assertEquals(Locale.FRENCH, session.getLocale());
124+
125+
signal.value(Locale.GERMAN);
126+
assertEquals(Locale.GERMAN, session.getLocale());
127+
128+
signal.value(Locale.JAPANESE);
129+
assertEquals(Locale.JAPANESE, session.getLocale());
130+
}
131+
}

0 commit comments

Comments
 (0)