Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge custom translations into client app i18n #592

Merged
merged 6 commits into from
Jan 19, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.obiba.agate.config.MetricsConfiguration;
import org.obiba.agate.domain.AgateRealm;
import org.obiba.agate.domain.Configuration;
import org.obiba.agate.domain.LocalizedString;
Expand All @@ -54,12 +55,17 @@
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;

import jakarta.inject.Inject;

import java.io.File;
import java.io.IOException;
import java.security.Key;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;

import static com.jayway.jsonpath.Configuration.defaultConfiguration;

Expand Down Expand Up @@ -90,12 +96,12 @@ public class ConfigurationService {

@Inject
public ConfigurationService(
AgateConfigRepository agateConfigRepository,
RealmConfigService realmConfigService,
EventBus eventBus,
Environment env,
ApplicationContext applicationContext,
ObjectMapper objectMapper) {
AgateConfigRepository agateConfigRepository,
RealmConfigService realmConfigService,
EventBus eventBus,
Environment env,
ApplicationContext applicationContext,
ObjectMapper objectMapper) {
this.agateConfigRepository = agateConfigRepository;
this.realmConfigService = realmConfigService;
this.eventBus = eventBus;
Expand Down Expand Up @@ -150,8 +156,8 @@ public Configuration getConfiguration() {
public void save(@Valid Configuration configuration) {
Configuration savedConfiguration = getOrCreateConfiguration();
BeanUtils
.copyProperties(configuration, savedConfiguration, "id", "version", "createdBy", "createdDate", "lastModifiedBy",
"lastModifiedDate", "secretKey", "agateVersion");
.copyProperties(configuration, savedConfiguration, "id", "version", "createdBy", "createdDate", "lastModifiedBy",
"lastModifiedDate", "secretKey", "agateVersion");
if (configuration.getAgateVersion() != null) savedConfiguration.setAgateVersion(configuration.getAgateVersion());
agateConfigRepository.save(savedConfiguration);
cachedConfiguration = null;
Expand Down Expand Up @@ -223,14 +229,14 @@ public JSONObject getJoinConfiguration(String locale, String application) throws
public JSONObject getJoinConfiguration(String locale, String application, boolean forEditing) throws JSONException, IOException {
Configuration config = getConfiguration();
List<RealmConfig> realms = Strings.isNullOrEmpty(application)
? realmConfigService.findAllRealmsForSignup()
: realmConfigService.findAllRealmsForSignupAndApplication(application);
? realmConfigService.findAllRealmsForSignup()
: realmConfigService.findAllRealmsForSignupAndApplication(application);

JSONObject form = UserFormBuilder.newBuilder(config, locale, applicationContext.getResource("classpath:join/formDefinition.json"))
.realms(realms)
.attributes(config.getUserAttributes())
.addUsername(forEditing || config.isJoinWithUsername())
.build();
.realms(realms)
.attributes(config.getUserAttributes())
.addUsername(forEditing || config.isJoinWithUsername())
.build();

return new TranslationUtils().translate(form, getTranslator(locale));
}
Expand All @@ -247,9 +253,9 @@ public JSONObject getProfileConfiguration(String locale) throws JSONException, I
List<RealmConfig> realms = realmConfigService.findAllRealmsForSignup();

JSONObject form = UserFormBuilder.newBuilder(config, locale, applicationContext.getResource("classpath:join/formDefinition.json"))
.realms(realms)
.attributes(config.getUserAttributes())
.build();
.realms(realms)
.attributes(config.getUserAttributes())
.build();

return new TranslationUtils().translate(form, getTranslator(locale));
}
Expand All @@ -260,32 +266,32 @@ public JSONObject getRealmFormConfiguration(String locale, boolean forEditing) t
JSONObject form = new JSONObject();

form.put(
"form",
translationUtils.translate(RealmConfigFormBuilder.newBuilder(forEditing).build(), translator).toString()
"form",
translationUtils.translate(RealmConfigFormBuilder.newBuilder(forEditing).build(), translator).toString()
);
form.put(
"userInfoMapping",
translationUtils.translate(RealmUserInfoFormBuilder.newBuilder(extractUserInfoFieldsToMap(forEditing)).build(), translator).toString()
"userInfoMapping",
translationUtils.translate(RealmUserInfoFormBuilder.newBuilder(extractUserInfoFieldsToMap(forEditing)).build(), translator).toString()
);
form.put(
"userInfoMappingDefaults",
UserInfoFieldsMappingDefaultsFactory.create()
"userInfoMappingDefaults",
UserInfoFieldsMappingDefaultsFactory.create()
);
form.put(
AgateRealm.AGATE_LDAP_REALM.getName(),
translationUtils.translate(LdapRealmConfigFormBuilder.newBuilder().build(), translator).toString()
AgateRealm.AGATE_LDAP_REALM.getName(),
translationUtils.translate(LdapRealmConfigFormBuilder.newBuilder().build(), translator).toString()
);
form.put(
AgateRealm.AGATE_JDBC_REALM.getName(),
translationUtils.translate(JdbcRealmConfigFormBuilder.newBuilder().build().toString(), translator)
AgateRealm.AGATE_JDBC_REALM.getName(),
translationUtils.translate(JdbcRealmConfigFormBuilder.newBuilder().build().toString(), translator)
);
form.put(
AgateRealm.AGATE_AD_REALM.getName(),
translationUtils.translate(ActiveDirectoryRealmConfigFormBuilder.newBuilder().build().toString(), translator)
AgateRealm.AGATE_AD_REALM.getName(),
translationUtils.translate(ActiveDirectoryRealmConfigFormBuilder.newBuilder().build().toString(), translator)
);
form.put(
AgateRealm.AGATE_OIDC_REALM.getName(),
translationUtils.translate(OidcRealmConfigFormBuilder.newBuilder().build().toString(), translator)
AgateRealm.AGATE_OIDC_REALM.getName(),
translationUtils.translate(OidcRealmConfigFormBuilder.newBuilder().build().toString(), translator)
);

return form;
Expand Down Expand Up @@ -420,6 +426,46 @@ private Resource getTranslationsResource(String locale) {
return applicationContext.getResource(String.format("classpath:/i18n/%s.json", locale));
}

/**
* Update translations based on additions and removals of locales.
*
* @param savedConfig
* @param updatedConfig
*/
private void updateTranslations(Configuration savedConfig, Configuration updatedConfig) {
LocalizedString translations = updatedConfig.getTranslations();
if (translations == null) return;

HashSet<Locale> saved = new HashSet<>(savedConfig.getLocales());
HashSet<Locale> updated = new HashSet<>(updatedConfig.getLocales());

Set<Locale> addedLocales = new HashSet<>(updated);
addedLocales.removeAll(saved);
Set<Locale> removedLocales = new HashSet<>(saved);
removedLocales.removeAll(updated);
Set<Locale> commonLocales = new HashSet<>(saved);
commonLocales.retainAll(updated);

if (!removedLocales.isEmpty()) {
updatedConfig.getTranslations().keySet().removeIf(locale -> removedLocales.contains(Locale.forLanguageTag(locale)));
}

if (!addedLocales.isEmpty()) {
Locale defaultLocale = commonLocales.stream()
.filter(locale -> locale.getLanguage().equalsIgnoreCase("en"))
.findFirst()
.orElse(commonLocales.isEmpty() ? null : commonLocales.iterator().next());

for (Locale locale : addedLocales) {
if (defaultLocale != null) {
translations.put(locale.toLanguageTag(), updatedConfig.getTranslations().get(defaultLocale.toLanguageTag()));
} else {
translations.put(locale.toLanguageTag(), "{}");
}
}
}
}

/**
* Apply settings that are modified only internally.
*
Expand All @@ -431,6 +477,7 @@ public Configuration applyInternalSettings(Configuration updatedConfig) {
updatedConfig.setSecretOtp(savedConfig.getSecretOtp());
updatedConfig.setGroupsSeeded(savedConfig.isGroupsSeeded());
updatedConfig.setApplicationsSeeded(savedConfig.isApplicationsSeeded());
updateTranslations(savedConfig, updatedConfig);
return updatedConfig;
}
}
35 changes: 35 additions & 0 deletions agate-ui/src/boot/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { boot } from 'quasar/wrappers';
import { createI18n } from 'vue-i18n';
import messages from 'src/i18n';
import { Quasar, Cookies } from 'quasar';
import { translationAsMap } from 'src/utils/translations';
import type { AttributeDto } from 'src/models/Agate';

export type MessageLanguages = keyof typeof messages;
// Type-define 'en-US' as the master schema for the resource
Expand Down Expand Up @@ -40,6 +42,28 @@ function getCurrentLocale(): string {
return detectedLocale || locales[0] || 'en';
}

function mergeWithCustomMessages() {
const serverTranslations = translationAsMap(systemStore.configuration.translations || []);

Object.keys(serverTranslations).forEach((lang) => {
const existingMessages = i18n.global.getLocaleMessage(lang) || {};
const newMessages = (serverTranslations[lang] || ([] as AttributeDto[])).reduce(
(acc, tr) => {
if (tr.name && tr.value) {
acc[tr.name] = tr.value;
}
return acc;
},
{} as Record<string, string>,
);

i18n.global.setLocaleMessage(lang, {
...existingMessages,
...newMessages,
});
});
}

const i18n = createI18n<{ message: MessageSchema }, MessageLanguages>({
locale: getCurrentLocale(),
fallbackLocale: locales[0] || 'en',
Expand All @@ -53,6 +77,17 @@ export default boot(({ app }) => {
app.use(i18n);
});

const systemStore = useSystemStore();

watch(
() => systemStore.configuration.translations,
(newValue) => {
if (newValue) {
mergeWithCustomMessages();
}
},
);

const t = i18n.global.t;

export { i18n, t, locales, getCurrentLocale };
Loading