diff --git a/README.md b/README.md
index cd4199b..0ed705c 100644
--- a/README.md
+++ b/README.md
@@ -126,6 +126,7 @@ Camoufox will automatically add the following default fonts associated your spoo
**Notes**:
- **navigator.webdriver** is set to false at all times.
+- `navigator.language` & `navigator.languages` will fall back to the `locale:language`/`locale:region` values if not set.
- When spoofing Chrome fingerprints, the following may leak:
- navigator.userAgentData missing.
- navigator.deviceMemory missing.
@@ -264,21 +265,26 @@ Camoufox can override the following network headers:
-Geolocation
+Geolocation & Intl
-| Property | Status | Description |
-| --------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| geolocation:latitude | ✅ | Latitude to use. Requires `geolocation:longitude` to be set as well. |
-| geolocation:longitude | ✅ | Longitude to use. Requires `geolocation:longitude` to be set as well. |
-| geolocation:accuracy | ✅ | Accuracy in meters. This will be calculated automatically using the decminal percision of `geolocation:latitude` & `geolocation:longitude` if not set. |
-| timezone | ✅ | Set a custom TZ timezone (e.g. "America/Chicago"). This will also change `Date()` to return the local time. |
+| Property | Status | Description | Required Keys |
+| --------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------- |
+| geolocation:latitude | ✅ | Latitude to use. | `geolocation:longitude` |
+| geolocation:longitude | ✅ | Longitude to use. | `geolocation:latitude` |
+| geolocation:accuracy | ✅ | Accuracy in meters. This will be calculated automatically using the decminal percision of `geolocation:latitude` & `geolocation:longitude` if not set. | |
+| timezone | ✅ | Set a custom TZ timezone (e.g. "America/Chicago"). This will also change `Date()` to return the local time. | |
+| locale:language | ✅ | Spoof the Intl API, headers, and system language (e.g. "en") | `locale:region` |
+| locale:region | ✅ | Spoof the Intl API, headers, and system region (e.g. "US"). | `locale:language` |
+| locale:script | ✅ | Set a custom script (e.g. "Latn"). Will be set automatically if not specified. | |
+
+The **Required Keys** are keys that must also be set for the property to work.
**Notes:**
-- Setting `geolocation:latitude` & `geolocation:longitude` will automatically accept Location permission prompts.
+- Location permission prompts will be accepted automatically if `geolocation:latitude` and `geolocation:longitude` are set.
- `timezone` **must** be set to a valid TZ identifier. See [here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for a list of valid timezones.
-- To change your locale, set the `intl.accept_languages` preference.
+- `locale:language` & `locale:region` **must** be set to valid locale values. See [here](https://simplelocalize.io/data/locales/) for a list of valid locale-region values.
@@ -406,7 +412,7 @@ Miscellaneous (battery status, etc)
- Support for spoofing both inner and outer window viewport sizes
- Network headers (Accept-Languages and User-Agent) are spoofed to match the navigator properties
- WebRTC IP spoofing at the protocol level
-- Geolocation & timezone spoofing
+- Geolocation, timezone, and locale spoofing
- etc.
#### Anti font fingerprinting
diff --git a/patches/chromeutil.patch b/patches/chromeutil.patch
index 5641f99..b689e49 100644
--- a/patches/chromeutil.patch
+++ b/patches/chromeutil.patch
@@ -1,5 +1,5 @@
diff --git a/dom/base/ChromeUtils.cpp b/dom/base/ChromeUtils.cpp
-index 6833d2227f..d3d2ef088d 100644
+index 6833d2227f..0b2ea99615 100644
--- a/dom/base/ChromeUtils.cpp
+++ b/dom/base/ChromeUtils.cpp
@@ -5,6 +5,7 @@
@@ -10,13 +10,12 @@ index 6833d2227f..d3d2ef088d 100644
#include "JSOracleParent.h"
#include "js/CallAndConstruct.h" // JS::Call
-@@ -2068,6 +2069,25 @@ bool ChromeUtils::IsDarkBackground(GlobalObject&, Element& aElement) {
+@@ -2068,6 +2069,24 @@ bool ChromeUtils::IsDarkBackground(GlobalObject&, Element& aElement) {
return nsNativeTheme::IsDarkBackground(f);
}
+/* static */
-+void ChromeUtils::CamouDebug(GlobalObject& aGlobal,
-+ const nsAString& aVarName) {
++void ChromeUtils::CamouDebug(GlobalObject& aGlobal, const nsAString& aVarName) {
+ if (auto value = MaskConfig::GetBool("debug");
+ value.has_value() && !value.value()) {
+ return;
@@ -36,13 +35,13 @@ index 6833d2227f..d3d2ef088d 100644
double ChromeUtils::DateNow(GlobalObject&) { return JS_Now() / 1000.0; }
/* static */
-@@ -2094,6 +2114,28 @@ void ChromeUtils::GetAllPossibleUtilityActorNames(GlobalObject& aGlobal,
+@@ -2094,6 +2113,39 @@ void ChromeUtils::GetAllPossibleUtilityActorNames(GlobalObject& aGlobal,
}
}
+/* static */
+int32_t ChromeUtils::CamouGetInt(GlobalObject& aGlobal,
-+ const nsAString& aVarName) {
++ const nsAString& aVarName) {
+ NS_ConvertUTF16toUTF8 utf8VarName(aVarName);
+ if (auto value = MaskConfig::GetInt32(utf8VarName.get())) {
+ return value.value();
@@ -52,8 +51,8 @@ index 6833d2227f..d3d2ef088d 100644
+
+/* static */
+double ChromeUtils::CamouGetDouble(GlobalObject& aGlobal,
-+ const nsAString& aVarName,
-+ double aDefaultValue) {
++ const nsAString& aVarName,
++ double aDefaultValue) {
+ NS_ConvertUTF16toUTF8 utf8VarName(aVarName);
+ if (auto value = MaskConfig::GetDouble(utf8VarName.get())) {
+ return value.value();
@@ -61,12 +60,23 @@ index 6833d2227f..d3d2ef088d 100644
+ return aDefaultValue;
+}
+
++/* static */
++void ChromeUtils::CamouGetString(GlobalObject& aGlobal,
++ const nsAString& aVarName,
++ nsAString& aRetVal) {
++ NS_ConvertUTF16toUTF8 utf8VarName(aVarName);
++ if (auto value = MaskConfig::GetString(utf8VarName.get())) {
++ aRetVal.Assign(NS_ConvertUTF8toUTF16(value.value()));
++ } else {
++ aRetVal.Truncate();
++ }
++}
+
/* static */
bool ChromeUtils::ShouldResistFingerprinting(
GlobalObject& aGlobal, JSRFPTarget aTarget,
diff --git a/dom/base/ChromeUtils.h b/dom/base/ChromeUtils.h
-index 0150c59670..d297ab5788 100644
+index 0150c59670..e3d203ed1a 100644
--- a/dom/base/ChromeUtils.h
+++ b/dom/base/ChromeUtils.h
@@ -301,6 +301,10 @@ class ChromeUtils {
@@ -80,7 +90,7 @@ index 0150c59670..d297ab5788 100644
static double DateNow(GlobalObject&);
static void EnsureJSOracleStarted(GlobalObject&);
-@@ -310,6 +314,11 @@ class ChromeUtils {
+@@ -310,6 +314,14 @@ class ChromeUtils {
static void GetAllPossibleUtilityActorNames(GlobalObject& aGlobal,
nsTArray& aNames);
@@ -88,12 +98,15 @@ index 0150c59670..d297ab5788 100644
+
+ static double CamouGetDouble(GlobalObject& aGlobal, const nsAString& aVarName,
+ double aDefaultValue);
++
++ static void CamouGetString(GlobalObject& aGlobal, const nsAString& aVarName,
++ nsAString& aRetVal);
+
static bool ShouldResistFingerprinting(
GlobalObject& aGlobal, JSRFPTarget aTarget,
const Nullable& aOverriddenFingerprintingSettings);
diff --git a/dom/chrome-webidl/ChromeUtils.webidl b/dom/chrome-webidl/ChromeUtils.webidl
-index bf196f039d..994b5803df 100644
+index bf196f039d..04e0cdcabd 100644
--- a/dom/chrome-webidl/ChromeUtils.webidl
+++ b/dom/chrome-webidl/ChromeUtils.webidl
@@ -746,6 +746,13 @@ partial namespace ChromeUtils {
@@ -110,7 +123,7 @@ index bf196f039d..994b5803df 100644
/**
* Starts the JSOracle process for ORB JavaScript validation, if it hasn't started already.
*/
-@@ -757,6 +764,16 @@ partial namespace ChromeUtils {
+@@ -757,6 +764,21 @@ partial namespace ChromeUtils {
[ChromeOnly]
readonly attribute unsigned long aliveUtilityProcesses;
@@ -123,6 +136,11 @@ index bf196f039d..994b5803df 100644
+ * Get a double value from Camoufox MaskConfig.
+ */
+ double camouGetDouble(DOMString varName, double defaultValue);
++
++ /**
++ * Get a string value from Camoufox MaskConfig.
++ */
++ DOMString camouGetString(DOMString varName);
+
/**
* Get a list of all possible Utility process Actor Names ; mostly useful to
diff --git a/patches/locale-spoofing.patch b/patches/locale-spoofing.patch
new file mode 100644
index 0000000..02523e9
--- /dev/null
+++ b/patches/locale-spoofing.patch
@@ -0,0 +1,186 @@
+diff --git a/browser/base/content/browser-init.js b/browser/base/content/browser-init.js
+index 16f4dd7d42..63c9c39741 100644
+--- a/browser/base/content/browser-init.js
++++ b/browser/base/content/browser-init.js
+@@ -109,6 +109,17 @@ var gBrowserInit = {
+ window.TabBarVisibility.update();
+ TabsInTitlebar.init();
+
++ let camouLocale;
++ let language = ChromeUtils.camouGetString("locale:language");
++ let region = ChromeUtils.camouGetString("locale:region");
++ if (language && region) {
++ camouLocale = language + "-" + region + ", " + language;
++ } else {
++ camouLocale = ChromeUtils.camouGetString("navigator.language");
++ }
++ if (camouLocale)
++ Services.prefs.setCharPref("intl.accept_languages", camouLocale);
++
+ new LightweightThemeConsumer(document);
+
+ if (
+diff --git a/intl/components/src/Locale.cpp b/intl/components/src/Locale.cpp
+index 9a043518cf..a8d3733212 100644
+--- a/intl/components/src/Locale.cpp
++++ b/intl/components/src/Locale.cpp
+@@ -24,11 +24,56 @@
+
+ #include "unicode/uloc.h"
+ #include "unicode/utypes.h"
++#include "MaskConfig.hpp"
+
+ namespace mozilla::intl {
+
+ using namespace intl::LanguageTagLimits;
+
++
++// Helper methods for header file
++
++const char* Locale::GetCamouLanguage() {
++ if (auto lang = MaskConfig::GetString("locale:language"))
++ return lang.value().c_str();
++ return nullptr;
++}
++
++const char* Locale::GetCamouRegion() {
++ if (auto region = MaskConfig::GetString("locale:region"))
++ return region.value().c_str();
++ return nullptr;
++}
++
++const char* Locale::GetCamouScript() {
++ if (auto script = MaskConfig::GetString("locale:script"))
++ return script.value().c_str();
++ return nullptr;
++}
++
++// Publicly exposed methods
++
++const char* Locale::GetCamouLocale() {
++ static std::string locale;
++
++ if (auto lang = MaskConfig::GetString("locale:language"))
++ if (auto region = MaskConfig::GetString("locale:region"))
++ locale = (lang.value() + "-" + region.value());
++ if (auto value = MaskConfig::GetString("navigator.language"))
++ locale = value.value();
++ if (locale.empty())
++ return nullptr;
++ return locale.c_str();
++}
++
++const char* Locale::GetDefaultLocale() {
++ // In Camoufox, GetDefaultLocale is defined outside the header
++ // to access MaskConfig.
++ if (auto value = GetCamouLocale())
++ return value;
++ return uloc_getDefault();
++}
++
+ template
+ bool IsStructurallyValidLanguageTag(Span aLanguage) {
+ // unicode_language_subtag = alpha{2,3} | alpha{5,8};
+diff --git a/intl/components/src/Locale.h b/intl/components/src/Locale.h
+index 1f4e06f543..448486a479 100644
+--- a/intl/components/src/Locale.h
++++ b/intl/components/src/Locale.h
+@@ -190,8 +190,11 @@ using UniqueChars = UniquePtr;
+ */
+ class MOZ_STACK_CLASS Locale final {
+ LanguageSubtag mLanguage = {};
++ mutable LanguageSubtag mCamouLanguage = {};
+ ScriptSubtag mScript = {};
++ mutable ScriptSubtag mCamouScript = {};
+ RegionSubtag mRegion = {};
++ mutable RegionSubtag mCamouRegion = {};
+
+ using VariantsVector = Vector;
+ using ExtensionsVector = Vector;
+@@ -314,9 +317,28 @@ class MOZ_STACK_CLASS Locale final {
+ }
+ };
+
+- const LanguageSubtag& Language() const { return mLanguage; }
+- const ScriptSubtag& Script() const { return mScript; }
+- const RegionSubtag& Region() const { return mRegion; }
++ const LanguageSubtag& Language() const {
++ if (const char* lang = GetCamouLanguage()) {
++ mCamouLanguage.Set(mozilla::Span(lang, strlen(lang)));
++ return mCamouLanguage;
++ }
++ return mLanguage;
++ }
++ const ScriptSubtag& Script() const {
++ if (const char* script = GetCamouScript()) {
++ mCamouScript.Set(mozilla::Span(script, strlen(script)));
++ return mCamouScript;
++ }
++ return mScript;
++ }
++ const RegionSubtag& Region() const {
++ if (const char* region = GetCamouRegion()) {
++ mCamouRegion.Set(mozilla::Span(region, strlen(region)));
++ return mCamouRegion;
++ }
++ return mRegion;
++ }
++
+ auto Variants() const { return SubtagEnumeration(mVariants); }
+ auto Extensions() const { return SubtagEnumeration(mExtensions); }
+ Maybe> PrivateUse() const {
+@@ -483,7 +505,15 @@ class MOZ_STACK_CLASS Locale final {
+ *
+ * Also see .
+ */
+- static const char* GetDefaultLocale() { return uloc_getDefault(); }
++ static const char* GetDefaultLocale();
++
++ static const char* GetCamouLocale();
++
++ static const char* GetCamouLanguage();
++
++ static const char* GetCamouRegion();
++
++ static const char* GetCamouScript();
+
+ /**
+ * Returns an iterator over all supported locales.
+diff --git a/intl/locale/OSPreferences.cpp b/intl/locale/OSPreferences.cpp
+index b87924b61a..2eaa960c04 100644
+--- a/intl/locale/OSPreferences.cpp
++++ b/intl/locale/OSPreferences.cpp
+@@ -406,6 +406,10 @@ bool OSPreferences::GetDateTimeConnectorPattern(const nsACString& aLocale,
+ */
+ NS_IMETHODIMP
+ OSPreferences::GetSystemLocales(nsTArray& aRetVal) {
++ if (const char* camouLocale = mozilla::intl::Locale::GetCamouLocale()) {
++ aRetVal.AppendElement(camouLocale);
++ return NS_OK;
++ }
+ if (!mSystemLocales.IsEmpty()) {
+ aRetVal = mSystemLocales.Clone();
+ return NS_OK;
+@@ -425,7 +429,10 @@ OSPreferences::GetSystemLocales(nsTArray& aRetVal) {
+
+ NS_IMETHODIMP
+ OSPreferences::GetSystemLocale(nsACString& aRetVal) {
+- if (!mSystemLocales.IsEmpty()) {
++ if (const char* camouLocale = mozilla::intl::Locale::GetCamouLocale()) {
++ aRetVal = camouLocale;
++ return NS_OK;
++ } else if (!mSystemLocales.IsEmpty()) {
+ aRetVal = mSystemLocales[0];
+ } else {
+ AutoTArray locales;
+@@ -439,6 +446,10 @@ OSPreferences::GetSystemLocale(nsACString& aRetVal) {
+
+ NS_IMETHODIMP
+ OSPreferences::GetRegionalPrefsLocales(nsTArray& aRetVal) {
++ if (const char* camouLocale = mozilla::intl::Locale::GetCamouLocale()) {
++ aRetVal.AppendElement(camouLocale);
++ return NS_OK;
++ }
+ if (!mRegionalPrefsLocales.IsEmpty()) {
+ aRetVal = mRegionalPrefsLocales.Clone();
+ return NS_OK;
diff --git a/settings/properties.json b/settings/properties.json
index 9b0be58..3bfefe7 100644
--- a/settings/properties.json
+++ b/settings/properties.json
@@ -58,5 +58,8 @@
{ "property": "geolocation:latitude", "type": "double" },
{ "property": "geolocation:longitude", "type": "double" },
{ "property": "geolocation:accuracy", "type": "double" },
- { "property": "timezone", "type": "str" }
+ { "property": "timezone", "type": "str" },
+ { "property": "locale:language", "type": "str" },
+ { "property": "locale:region", "type": "str" },
+ { "property": "locale:script", "type": "str" }
]
diff --git a/upstream.sh b/upstream.sh
index 3140952..d7683f2 100644
--- a/upstream.sh
+++ b/upstream.sh
@@ -1,2 +1,2 @@
version=130.0.1
-release=beta.7
+release=beta.8