diff --git a/bubble-server/src/main/java/bubble/model/account/Account.java b/bubble-server/src/main/java/bubble/model/account/Account.java index f98458ef..2f81de5d 100644 --- a/bubble-server/src/main/java/bubble/model/account/Account.java +++ b/bubble-server/src/main/java/bubble/model/account/Account.java @@ -17,6 +17,7 @@ import org.cobbzilla.wizard.filters.auth.TokenPrincipal; import org.cobbzilla.wizard.model.HashedPassword; import org.cobbzilla.wizard.model.Identifiable; import org.cobbzilla.wizard.model.IdentifiableBase; +import org.cobbzilla.wizard.model.entityconfig.EntityFieldType; import org.cobbzilla.wizard.model.entityconfig.annotations.*; import org.cobbzilla.wizard.model.search.SqlViewSearchResult; import org.cobbzilla.wizard.validation.ConstraintViolationBean; @@ -33,6 +34,7 @@ import java.util.Arrays; import java.util.List; import java.util.regex.Pattern; +import static bubble.server.BubbleConfiguration.DEFAULT_LOCALE; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.SECONDS; import static org.cobbzilla.util.daemon.ZillaRuntime.*; @@ -110,6 +112,11 @@ public class Account extends IdentifiableBase implements TokenPrincipal, SqlView @Type(type=ENCRYPTED_STRING) @Column(columnDefinition="varchar("+(10000+ENC_PAD)+")") @Getter @Setter private String description; + @ECSearchable @ECField(type=EntityFieldType.locale) + @Size(max=20, message="err.locale.length") + @Type(type=ENCRYPTED_STRING) @Column(columnDefinition="varchar("+(20+ENC_PAD)+") NOT NULL") + @Getter @Setter private String locale = DEFAULT_LOCALE; + @ECSearchable @Getter @Setter private Boolean admin = false; public boolean admin () { return admin != null && admin; } diff --git a/bubble-server/src/main/java/bubble/model/app/RuleDriver.java b/bubble-server/src/main/java/bubble/model/app/RuleDriver.java index dae185fc..e3bbbad7 100644 --- a/bubble-server/src/main/java/bubble/model/app/RuleDriver.java +++ b/bubble-server/src/main/java/bubble/model/app/RuleDriver.java @@ -3,6 +3,7 @@ package bubble.model.app; import bubble.model.account.Account; import bubble.model.account.AccountTemplate; import bubble.rule.AppRuleDriver; +import bubble.server.BubbleConfiguration; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.JsonNode; import lombok.Getter; @@ -21,6 +22,7 @@ import javax.persistence.*; import java.util.Locale; import static bubble.ApiConstants.*; +import static bubble.server.BubbleConfiguration.DEFAULT_LOCALE; import static org.cobbzilla.util.io.StreamUtil.stream2string; import static org.cobbzilla.util.json.JsonUtil.json; import static org.cobbzilla.util.reflect.ReflectionUtil.copy; @@ -98,7 +100,7 @@ public class RuleDriver extends IdentifiableBase implements AccountTemplate { @Transient @Getter @Setter private AppRuleDriverDescriptor descriptor; public AppRuleDriverDescriptor getDescriptor(Locale locale) { - final String localeString = locale != null ? locale.toString() : "en_US"; + final String localeString = locale != null ? locale.toString() : DEFAULT_LOCALE; final AppRuleDriverDescriptor localized; final String prefix = driverClass.replace(".", "/"); try { diff --git a/bubble-server/src/main/java/bubble/model/cloud/BubbleNetwork.java b/bubble-server/src/main/java/bubble/model/cloud/BubbleNetwork.java index 4ba4ddb8..8ec9612a 100644 --- a/bubble-server/src/main/java/bubble/model/cloud/BubbleNetwork.java +++ b/bubble-server/src/main/java/bubble/model/cloud/BubbleNetwork.java @@ -28,6 +28,7 @@ import static bubble.ApiConstants.EP_NETWORKS; import static bubble.ApiConstants.ROOT_NETWORK_UUID; import static bubble.model.cloud.BubbleDomain.DOMAIN_NAME_MAXLEN; import static bubble.model.cloud.BubbleNetworkState.created; +import static bubble.server.BubbleConfiguration.DEFAULT_LOCALE; import static org.cobbzilla.util.daemon.ZillaRuntime.die; import static org.cobbzilla.util.reflect.ReflectionUtil.copy; import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.ENCRYPTED_STRING; @@ -107,7 +108,7 @@ public class BubbleNetwork extends IdentifiableBase implements HasNetwork, HasBu @ECSearchable @ECField(type=EntityFieldType.locale) @Size(max=20, message="err.locale.length") @Type(type=ENCRYPTED_STRING) @Column(columnDefinition="varchar("+(20+ENC_PAD)+") NOT NULL") - @Getter @Setter private String locale = "en_US"; + @Getter @Setter private String locale = DEFAULT_LOCALE; // A unicode timezone alias from: cobbzilla-utils/src/main/resources/org/cobbzilla/util/time/unicode-timezones.xml // All unicode aliases are guaranteed to map to a Linux timezone and a Java timezone diff --git a/bubble-server/src/main/java/bubble/resources/account/MeResource.java b/bubble-server/src/main/java/bubble/resources/account/MeResource.java index 24dbc388..11fe34de 100644 --- a/bubble-server/src/main/java/bubble/resources/account/MeResource.java +++ b/bubble-server/src/main/java/bubble/resources/account/MeResource.java @@ -23,6 +23,7 @@ import bubble.service.cloud.StandardNetworkService; import com.fasterxml.jackson.databind.JsonNode; import lombok.Cleanup; import lombok.extern.slf4j.Slf4j; +import org.cobbzilla.util.string.LocaleUtil; import org.cobbzilla.wizard.auth.ChangePasswordRequest; import org.cobbzilla.wizard.client.ApiClientBase; import org.cobbzilla.wizard.client.script.ApiRunner; @@ -39,6 +40,7 @@ import javax.ws.rs.*; import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; import java.io.StringWriter; +import java.util.Locale; import static bubble.ApiConstants.*; import static org.cobbzilla.util.daemon.ZillaRuntime.errorString; @@ -69,6 +71,28 @@ public class MeResource { } } + @GET @Path(EP_LOCALE) + public Response getLocale(@Context ContainerRequest ctx) { + final Account account = userPrincipal(ctx); + return ok(account.getLocale()); + } + + @POST @Path(EP_LOCALE+"/{locale}") + public Response setLocale(@Context ContainerRequest ctx, + @PathParam("locale") String locale) { + final Account account = userPrincipal(ctx); + final Account me = accountDAO.findByUuid(account.getUuid()); + if (me == null) return notFound(); + + final Locale loc; + try { + loc = LocaleUtil.fromStringOrDie(locale); // must be valid + } catch (Exception e) { + return invalid("err.locale.invalid", "Invalid locale: "+locale, locale); + } + return ok(accountDAO.update(me.setLocale(loc.toString()))); + } + @POST @Path(EP_CHANGE_PASSWORD) public Response changePassword(@Context ContainerRequest ctx, ChangePasswordRequest request) { diff --git a/bubble-server/src/main/java/bubble/server/BubbleConfiguration.java b/bubble-server/src/main/java/bubble/server/BubbleConfiguration.java index 8bb9622f..324d8bf0 100644 --- a/bubble-server/src/main/java/bubble/server/BubbleConfiguration.java +++ b/bubble-server/src/main/java/bubble/server/BubbleConfiguration.java @@ -65,6 +65,7 @@ public class BubbleConfiguration extends PgRestServerConfiguration public static final String TAG_PAYMENTS_ENABLED = "paymentsEnabled"; public static final String TAG_CLOUD_DRIVERS = "cloudDrivers"; public static final String TAG_ENTITY_CLASSES = "entityClasses"; + public static final String TAG_LOCALES = "locales"; public static final String DEFAULT_LOCALE = "en_US"; public static final String DEFAULT_LOCAL_STORAGE_DIR = HOME_DIR + "/.bubble_local_storage"; @@ -237,12 +238,15 @@ public class BubbleConfiguration extends PgRestServerConfiguration {TAG_SAGE_LAUNCHER, isSageLauncher()}, {TAG_PAYMENTS_ENABLED, paymentsEnabled()}, {TAG_CLOUD_DRIVERS, getCloudDriverClasses()}, - {TAG_ENTITY_CLASSES, getSortedEntityClassNames()} + {TAG_ENTITY_CLASSES, getSortedEntityClassNames()}, + {TAG_LOCALES, getAllLocales()} })); } return publicSystemConfigs.get(); } } + + // called after activation, because now thisNetwork will be defined. otherwise it remains unchanged public void refreshPublicSystemConfigs () { synchronized (publicSystemConfigs) { publicSystemConfigs.set(null); } } @Getter @Setter private String[] disallowedCountries; diff --git a/bubble-server/src/main/resources/message_templates/en_US/server/post_auth/ResourceMessages.properties b/bubble-server/src/main/resources/message_templates/en_US/server/post_auth/ResourceMessages.properties index caa0fa2f..93807e51 100644 --- a/bubble-server/src/main/resources/message_templates/en_US/server/post_auth/ResourceMessages.properties +++ b/bubble-server/src/main/resources/message_templates/en_US/server/post_auth/ResourceMessages.properties @@ -153,8 +153,6 @@ message_no_verified_contacts_subtext=Before creating your first Bubble, please v form_title_new_network=New Bubble field_label_network_name=Name field_label_network_domain=Domain -field_label_locale=Language -field_label_timezone=Time Zone field_label_plan=Plan field_label_region=Location field_label_footprint=Footprint @@ -180,10 +178,6 @@ footprint_description_EU=Your Bubble will only run within European Union countri footprint_name_Worldwide=World-wide footprint_description_Worldwide=Your Bubble can run anywhere in the world -# Supported locales -locale_codes=en_US -locale_en_US=English (US) - # Payment methods payment_description_credit=Credit or Debit Card payment_description_code=Invitation Code @@ -330,8 +324,6 @@ err.label.length=Label is too long err.latlon.invalid=lat/lon is invalid err.list.failed=listing failed for prefix err.listNext.failed=listing next batch failed for id -err.locale.length=Locale is too long -err.locale.required=Locale is required err.name.invalid=Name is invalid err.name.regexFailed=Name must start with a letter and can only contain letters, numbers, hyphens, periods and underscores err.node.name.alreadyExists=A node already exists with the same FQDN @@ -434,9 +426,6 @@ err.tgzB64.invalid.missingTasksMainYml=No tasks/main.yml file found for role in err.tgzB64.invalid.writingToStorage=Error writing tgz to storage err.tgzB64.invalid.readingFromStorage=Error reading tgz from storage err.tgzB64.required=tgzB64 is required -err.timezone.unknown=An error ocurred trying to determine the time zone -err.timezone.length=Time zone is too long -err.timezone.required=Time zone is requird err.totpKey.length=TOTP key is required err.type.notVerifiable=Type is not verifiable err.type.invalid=Type is invalid diff --git a/bubble-server/src/main/resources/message_templates/en_US/server/pre_auth/ResourceMessages.properties b/bubble-server/src/main/resources/message_templates/en_US/server/pre_auth/ResourceMessages.properties index 1bad724e..10cc586e 100644 --- a/bubble-server/src/main/resources/message_templates/en_US/server/pre_auth/ResourceMessages.properties +++ b/bubble-server/src/main/resources/message_templates/en_US/server/pre_auth/ResourceMessages.properties @@ -8,6 +8,27 @@ message_false=False message_null=null message_undefined=undefined +button_label_set_locale=Set Language +field_label_locale=Language +field_label_timezone=Time Zone + +# Locales +locale_codes=en_US,fr_FR,es_ES,it_IT,de_DE,en_GB,es_US +locale_en_US=English (US) +locale_es_US=Spanish (US) +locale_es_ES=Spanish +locale_it_IT=Italian +locale_fr_FR=French +locale_de_DE=German +locale_en_GB=English (GB) +locale_detect=Auto Detect +err.locale.invalid=Locale is invalid +err.locale.length=Locale is too long +err.locale.required=Locale is required +err.timezone.unknown=An error occurred trying to determine the time zone +err.timezone.length=Time zone is too long +err.timezone.required=Time zone is required + err.name.required=Name is required err.name.tooShort=Name must be at least 4 characters err.name.tooLong=Name cannot be longer than 100 characters diff --git a/bubble-web b/bubble-web index 3eab9042..7426503b 160000 --- a/bubble-web +++ b/bubble-web @@ -1 +1 @@ -Subproject commit 3eab904291f158ab130c549a2bdd15bb77f0ec03 +Subproject commit 7426503bbf995c3ff73ec8ea33bee1af83b8d3e1 diff --git a/utils/cobbzilla-utils b/utils/cobbzilla-utils index 9087017e..7437196e 160000 --- a/utils/cobbzilla-utils +++ b/utils/cobbzilla-utils @@ -1 +1 @@ -Subproject commit 9087017eda08a97479010705e00014d31e8ccec1 +Subproject commit 7437196eaf77b7426f43264a781f9f0457ff710f