@@ -17,6 +17,7 @@ import org.cobbzilla.wizard.filters.auth.TokenPrincipal; | |||||
import org.cobbzilla.wizard.model.HashedPassword; | import org.cobbzilla.wizard.model.HashedPassword; | ||||
import org.cobbzilla.wizard.model.Identifiable; | import org.cobbzilla.wizard.model.Identifiable; | ||||
import org.cobbzilla.wizard.model.IdentifiableBase; | 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.entityconfig.annotations.*; | ||||
import org.cobbzilla.wizard.model.search.SqlViewSearchResult; | import org.cobbzilla.wizard.model.search.SqlViewSearchResult; | ||||
import org.cobbzilla.wizard.validation.ConstraintViolationBean; | import org.cobbzilla.wizard.validation.ConstraintViolationBean; | ||||
@@ -33,6 +34,7 @@ import java.util.Arrays; | |||||
import java.util.List; | import java.util.List; | ||||
import java.util.regex.Pattern; | 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.MILLISECONDS; | ||||
import static java.util.concurrent.TimeUnit.SECONDS; | import static java.util.concurrent.TimeUnit.SECONDS; | ||||
import static org.cobbzilla.util.daemon.ZillaRuntime.*; | 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)+")") | @Type(type=ENCRYPTED_STRING) @Column(columnDefinition="varchar("+(10000+ENC_PAD)+")") | ||||
@Getter @Setter private String description; | @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 | @ECSearchable | ||||
@Getter @Setter private Boolean admin = false; | @Getter @Setter private Boolean admin = false; | ||||
public boolean admin () { return admin != null && admin; } | public boolean admin () { return admin != null && admin; } | ||||
@@ -3,6 +3,7 @@ package bubble.model.app; | |||||
import bubble.model.account.Account; | import bubble.model.account.Account; | ||||
import bubble.model.account.AccountTemplate; | import bubble.model.account.AccountTemplate; | ||||
import bubble.rule.AppRuleDriver; | import bubble.rule.AppRuleDriver; | ||||
import bubble.server.BubbleConfiguration; | |||||
import com.fasterxml.jackson.annotation.JsonIgnore; | import com.fasterxml.jackson.annotation.JsonIgnore; | ||||
import com.fasterxml.jackson.databind.JsonNode; | import com.fasterxml.jackson.databind.JsonNode; | ||||
import lombok.Getter; | import lombok.Getter; | ||||
@@ -21,6 +22,7 @@ import javax.persistence.*; | |||||
import java.util.Locale; | import java.util.Locale; | ||||
import static bubble.ApiConstants.*; | import static bubble.ApiConstants.*; | ||||
import static bubble.server.BubbleConfiguration.DEFAULT_LOCALE; | |||||
import static org.cobbzilla.util.io.StreamUtil.stream2string; | import static org.cobbzilla.util.io.StreamUtil.stream2string; | ||||
import static org.cobbzilla.util.json.JsonUtil.json; | import static org.cobbzilla.util.json.JsonUtil.json; | ||||
import static org.cobbzilla.util.reflect.ReflectionUtil.copy; | 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; | @Transient @Getter @Setter private AppRuleDriverDescriptor descriptor; | ||||
public AppRuleDriverDescriptor getDescriptor(Locale locale) { | 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 AppRuleDriverDescriptor localized; | ||||
final String prefix = driverClass.replace(".", "/"); | final String prefix = driverClass.replace(".", "/"); | ||||
try { | try { | ||||
@@ -28,6 +28,7 @@ import static bubble.ApiConstants.EP_NETWORKS; | |||||
import static bubble.ApiConstants.ROOT_NETWORK_UUID; | import static bubble.ApiConstants.ROOT_NETWORK_UUID; | ||||
import static bubble.model.cloud.BubbleDomain.DOMAIN_NAME_MAXLEN; | import static bubble.model.cloud.BubbleDomain.DOMAIN_NAME_MAXLEN; | ||||
import static bubble.model.cloud.BubbleNetworkState.created; | 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.daemon.ZillaRuntime.die; | ||||
import static org.cobbzilla.util.reflect.ReflectionUtil.copy; | import static org.cobbzilla.util.reflect.ReflectionUtil.copy; | ||||
import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.ENCRYPTED_STRING; | 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) | @ECSearchable @ECField(type=EntityFieldType.locale) | ||||
@Size(max=20, message="err.locale.length") | @Size(max=20, message="err.locale.length") | ||||
@Type(type=ENCRYPTED_STRING) @Column(columnDefinition="varchar("+(20+ENC_PAD)+") NOT NULL") | @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 | // 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 | // All unicode aliases are guaranteed to map to a Linux timezone and a Java timezone | ||||
@@ -23,6 +23,7 @@ import bubble.service.cloud.StandardNetworkService; | |||||
import com.fasterxml.jackson.databind.JsonNode; | import com.fasterxml.jackson.databind.JsonNode; | ||||
import lombok.Cleanup; | import lombok.Cleanup; | ||||
import lombok.extern.slf4j.Slf4j; | import lombok.extern.slf4j.Slf4j; | ||||
import org.cobbzilla.util.string.LocaleUtil; | |||||
import org.cobbzilla.wizard.auth.ChangePasswordRequest; | import org.cobbzilla.wizard.auth.ChangePasswordRequest; | ||||
import org.cobbzilla.wizard.client.ApiClientBase; | import org.cobbzilla.wizard.client.ApiClientBase; | ||||
import org.cobbzilla.wizard.client.script.ApiRunner; | 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.Context; | ||||
import javax.ws.rs.core.Response; | import javax.ws.rs.core.Response; | ||||
import java.io.StringWriter; | import java.io.StringWriter; | ||||
import java.util.Locale; | |||||
import static bubble.ApiConstants.*; | import static bubble.ApiConstants.*; | ||||
import static org.cobbzilla.util.daemon.ZillaRuntime.errorString; | 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) | @POST @Path(EP_CHANGE_PASSWORD) | ||||
public Response changePassword(@Context ContainerRequest ctx, | public Response changePassword(@Context ContainerRequest ctx, | ||||
ChangePasswordRequest request) { | ChangePasswordRequest request) { | ||||
@@ -65,6 +65,7 @@ public class BubbleConfiguration extends PgRestServerConfiguration | |||||
public static final String TAG_PAYMENTS_ENABLED = "paymentsEnabled"; | public static final String TAG_PAYMENTS_ENABLED = "paymentsEnabled"; | ||||
public static final String TAG_CLOUD_DRIVERS = "cloudDrivers"; | public static final String TAG_CLOUD_DRIVERS = "cloudDrivers"; | ||||
public static final String TAG_ENTITY_CLASSES = "entityClasses"; | 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_LOCALE = "en_US"; | ||||
public static final String DEFAULT_LOCAL_STORAGE_DIR = HOME_DIR + "/.bubble_local_storage"; | 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_SAGE_LAUNCHER, isSageLauncher()}, | ||||
{TAG_PAYMENTS_ENABLED, paymentsEnabled()}, | {TAG_PAYMENTS_ENABLED, paymentsEnabled()}, | ||||
{TAG_CLOUD_DRIVERS, getCloudDriverClasses()}, | {TAG_CLOUD_DRIVERS, getCloudDriverClasses()}, | ||||
{TAG_ENTITY_CLASSES, getSortedEntityClassNames()} | |||||
{TAG_ENTITY_CLASSES, getSortedEntityClassNames()}, | |||||
{TAG_LOCALES, getAllLocales()} | |||||
})); | })); | ||||
} | } | ||||
return publicSystemConfigs.get(); | return publicSystemConfigs.get(); | ||||
} | } | ||||
} | } | ||||
// called after activation, because now thisNetwork will be defined. otherwise it remains unchanged | |||||
public void refreshPublicSystemConfigs () { synchronized (publicSystemConfigs) { publicSystemConfigs.set(null); } } | public void refreshPublicSystemConfigs () { synchronized (publicSystemConfigs) { publicSystemConfigs.set(null); } } | ||||
@Getter @Setter private String[] disallowedCountries; | @Getter @Setter private String[] disallowedCountries; | ||||
@@ -153,8 +153,6 @@ message_no_verified_contacts_subtext=Before creating your first Bubble, please v | |||||
form_title_new_network=New Bubble | form_title_new_network=New Bubble | ||||
field_label_network_name=Name | field_label_network_name=Name | ||||
field_label_network_domain=Domain | field_label_network_domain=Domain | ||||
field_label_locale=Language | |||||
field_label_timezone=Time Zone | |||||
field_label_plan=Plan | field_label_plan=Plan | ||||
field_label_region=Location | field_label_region=Location | ||||
field_label_footprint=Footprint | 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_name_Worldwide=World-wide | ||||
footprint_description_Worldwide=Your Bubble can run anywhere in the world | 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 methods | ||||
payment_description_credit=Credit or Debit Card | payment_description_credit=Credit or Debit Card | ||||
payment_description_code=Invitation Code | payment_description_code=Invitation Code | ||||
@@ -330,8 +324,6 @@ err.label.length=Label is too long | |||||
err.latlon.invalid=lat/lon is invalid | err.latlon.invalid=lat/lon is invalid | ||||
err.list.failed=listing failed for prefix | err.list.failed=listing failed for prefix | ||||
err.listNext.failed=listing next batch failed for id | 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.invalid=Name is invalid | ||||
err.name.regexFailed=Name must start with a letter and can only contain letters, numbers, hyphens, periods and underscores | 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 | 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.writingToStorage=Error writing tgz to storage | ||||
err.tgzB64.invalid.readingFromStorage=Error reading tgz from storage | err.tgzB64.invalid.readingFromStorage=Error reading tgz from storage | ||||
err.tgzB64.required=tgzB64 is required | 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.totpKey.length=TOTP key is required | ||||
err.type.notVerifiable=Type is not verifiable | err.type.notVerifiable=Type is not verifiable | ||||
err.type.invalid=Type is invalid | err.type.invalid=Type is invalid | ||||
@@ -8,6 +8,27 @@ message_false=False | |||||
message_null=null | message_null=null | ||||
message_undefined=undefined | 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.required=Name is required | ||||
err.name.tooShort=Name must be at least 4 characters | err.name.tooShort=Name must be at least 4 characters | ||||
err.name.tooLong=Name cannot be longer than 100 characters | err.name.tooLong=Name cannot be longer than 100 characters | ||||
@@ -1 +1 @@ | |||||
Subproject commit 3eab904291f158ab130c549a2bdd15bb77f0ec03 | |||||
Subproject commit 7426503bbf995c3ff73ec8ea33bee1af83b8d3e1 |
@@ -1 +1 @@ | |||||
Subproject commit 9087017eda08a97479010705e00014d31e8ccec1 | |||||
Subproject commit 7437196eaf77b7426f43264a781f9f0457ff710f |