diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java index 7336c1b40..384e7f1a9 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java @@ -707,7 +707,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements */ private void buildDepictionList(List idAndCaptions) { binding.mediaDetailDepictionContainer.removeAllViews(); - String locale = Locale.getDefault().getLanguage(); + String fullCode = Locale.getDefault().getLanguage(); + String locale = fullCode.contains(",") ? fullCode.substring(0, fullCode.indexOf(',')).trim() : fullCode; for (IdAndCaptions idAndCaption : idAndCaptions) { binding.mediaDetailDepictionContainer.addView(buildDepictLabel( getDepictionCaption(idAndCaption, locale), diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java index 8d6b74231..dd041d8bc 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java @@ -409,15 +409,55 @@ public class OkHttpJsonApiClient { */ @Nullable public List getPlaces( - final List placeList, final String language) throws IOException { + final List placeList, final String language, final String secondaryLanguages) throws IOException { final String wikidataQuery = FileUtils.readFromResource("/queries/query_for_item.rq"); + final String[] secondaryLanguageArray = secondaryLanguages.split(",\\s*"); // could be used to generate backup SparQL Queries + String qids = ""; for (final Place place : placeList) { qids += "\n" + ("wd:" + place.getWikiDataEntityId()); } + + StringBuilder fallBackDescription = new StringBuilder(); + for (int i = 0; i < secondaryLanguageArray.length; i++) { + fallBackDescription.append("OPTIONAL {?item schema:description ?itemDescriptionPreferredLanguage_") + .append(i + 1) + .append(". FILTER (lang(?itemDescriptionPreferredLanguage_") + .append(i + 1) + .append(") = \"") + .append(secondaryLanguageArray[i]) + .append("\")}\n"); + } + + StringBuilder fallbackLabel = new StringBuilder(); + for (int i = 0; i < secondaryLanguageArray.length; i++) { + fallbackLabel.append("OPTIONAL {?item rdfs:label ?itemLabelPreferredLanguage_") + .append(i + 1) + .append(". FILTER (lang(?itemLabelPreferredLanguage_") + .append(i + 1) + .append(") = \"") + .append(secondaryLanguageArray[i]) + .append("\")}\n"); + } + + StringBuilder fallbackClassLabel = new StringBuilder(); + for (int i = 0; i < secondaryLanguageArray.length; i++) { + fallbackClassLabel.append("OPTIONAL {?class rdfs:label ?classLabelPreferredLanguage_") + .append(i + 1) + .append(". FILTER (lang(?classLabelPreferredLanguage_") + .append(i + 1) + .append(") = \"") + .append(secondaryLanguageArray[i]) + .append("\")}\n"); + } + final String query = wikidataQuery .replace("${ENTITY}", qids) - .replace("${LANG}", language); + .replace("${LANG}", language) + .replace("${SECONDARYDESCRIPTION}", fallBackDescription.toString()) + .replace("${SECONDARYLABEL}", fallbackLabel.toString()) + .replace("${SECONDARYCLASSLABEL}", fallbackClassLabel.toString()); + final HttpUrl.Builder urlBuilder = HttpUrl .parse(sparqlQueryUrl) .newBuilder() diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java index 7bb311961..d5f551e27 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java @@ -8,6 +8,8 @@ import androidx.annotation.Nullable; import fr.free.nrw.commons.BaseMarker; import fr.free.nrw.commons.MapController; import fr.free.nrw.commons.location.LatLng; +import fr.free.nrw.commons.kvstore.JsonKvStore; +import fr.free.nrw.commons.settings.Prefs; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -16,6 +18,7 @@ import java.util.ListIterator; import java.util.Locale; import java.util.Map; import javax.inject.Inject; +import javax.inject.Named; import timber.log.Timber; public class NearbyController extends MapController { @@ -34,6 +37,9 @@ public class NearbyController extends MapController { this.nearbyPlaces = nearbyPlaces; } + @Inject + @Named("default_preferences") + JsonKvStore defaultKvStore; /** * Prepares Place list to make their distance information update later. @@ -56,6 +62,7 @@ public class NearbyController extends MapController { Timber.d("Loading attractions nearby, but currentLatLng is null"); return null; } + List places = nearbyPlaces .radiusExpander(searchLatLng, Locale.getDefault().getLanguage(), returnClosestResult, customQuery); @@ -139,7 +146,10 @@ public class NearbyController extends MapController { * @throws Exception If an error occurs during the retrieval process. */ public List getPlaces(List placeList) throws Exception { - return nearbyPlaces.getPlaces(placeList, Locale.getDefault().getLanguage()); + + String secondaryLanguages = defaultKvStore.getString(Prefs.SECONDARY_LANGUAGE, ""); + + return nearbyPlaces.getPlaces(placeList, Locale.getDefault().getLanguage(), secondaryLanguages); } public static LatLng calculateNorthEast(double latitude, double longitude, double distance) { diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java index 46f0a2a9e..cfc45945e 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java @@ -131,9 +131,9 @@ public class NearbyPlaces { * @throws Exception If an error occurs during the retrieval process. */ public List getPlaces(final List placeList, - final String lang) throws Exception { + final String lang, final String lang2) throws Exception { return okHttpJsonApiClient - .getPlaces(placeList, lang); + .getPlaces(placeList, lang, lang2); } /** diff --git a/app/src/main/java/fr/free/nrw/commons/recentlanguages/SavedLanguagesAdapter.kt b/app/src/main/java/fr/free/nrw/commons/recentlanguages/SavedLanguagesAdapter.kt new file mode 100644 index 000000000..c067a6e49 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/recentlanguages/SavedLanguagesAdapter.kt @@ -0,0 +1,77 @@ +package fr.free.nrw.commons.recentlanguages + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import fr.free.nrw.commons.R +import fr.free.nrw.commons.databinding.RowItemLanguagesSpinnerBinding +import fr.free.nrw.commons.utils.LangCodeUtils +import org.apache.commons.lang3.StringUtils +import java.util.HashMap + +/** + * Array adapter for saved languages + */ +class SavedLanguagesAdapter constructor( + context: Context, + var savedLanguages: List, // List of saved languages + private val selectedLanguages: HashMap<*, String>, // Selected languages map +) : ArrayAdapter(context, R.layout.row_item_languages_spinner) { + /** + * Selected language code in SavedLanguagesAdapter + * Used for marking selected ones + */ + var selectedLangCode = "" + + override fun isEnabled(position: Int) = + savedLanguages[position].languageCode.let { + it.isNotEmpty() && !selectedLanguages.containsValue(it) && it != selectedLangCode + } + + override fun getCount() = savedLanguages.size + + override fun getView( + position: Int, + convertView: View?, + parent: ViewGroup, + ): View { + val binding: RowItemLanguagesSpinnerBinding + var rowView = convertView + + if (rowView == null) { + val layoutInflater = + context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + binding = RowItemLanguagesSpinnerBinding.inflate(layoutInflater, parent, false) + rowView = binding.root + } else { + binding = RowItemLanguagesSpinnerBinding.bind(rowView) + } + + val languageCode = savedLanguages[position].languageCode + val languageName = savedLanguages[position].languageName + binding.tvLanguage.let { + it.isEnabled = isEnabled(position) + if (languageCode.isEmpty()) { + it.text = StringUtils.capitalize(languageName) + it.textAlignment = View.TEXT_ALIGNMENT_CENTER + } else { + it.text = + "${StringUtils.capitalize(languageName)}" + + " [${LangCodeUtils.fixLanguageCode(languageCode)}]" + } + } + return rowView + } + + /** + * Provides code of a language from saved languages for a specific position + */ + fun getLanguageCode(position: Int): String = savedLanguages[position].languageCode + + /** + * Provides name of a language from saved languages for a specific position + */ + fun getLanguageName(position: Int): String = savedLanguages[position].languageName +} diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java index fee322644..01224b22c 100644 --- a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java @@ -18,6 +18,8 @@ import android.widget.AdapterView.OnItemClickListener; import android.widget.EditText; import android.widget.ListView; import android.widget.TextView; +import android.widget.Toast; +import android.widget.ArrayAdapter; import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; @@ -47,10 +49,13 @@ import fr.free.nrw.commons.logging.CommonsLogSender; import fr.free.nrw.commons.recentlanguages.Language; import fr.free.nrw.commons.recentlanguages.RecentLanguagesAdapter; import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao; +import fr.free.nrw.commons.recentlanguages.SavedLanguagesAdapter; import fr.free.nrw.commons.upload.LanguagesAdapter; import fr.free.nrw.commons.utils.DialogUtil; import fr.free.nrw.commons.utils.PermissionUtils; import fr.free.nrw.commons.utils.ViewUtil; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -153,7 +158,6 @@ public class SettingsFragment extends PreferenceFragmentCompat { appUiLanguageListPreference.setOnPreferenceClickListener(new OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { - System.out.println("Clicked appui"); prepareAppLanguages(appUiLanguageListPreference.getKey()); return true; } @@ -180,24 +184,18 @@ public class SettingsFragment extends PreferenceFragmentCompat { } }); + descriptionSecondaryLanguageListPreference = findPreference("descriptionSecondaryLanguagePref"); assert descriptionSecondaryLanguageListPreference != null; keyLanguageListPreference = descriptionSecondaryLanguageListPreference.getKey(); languageCode = getCurrentLanguageCode(keyLanguageListPreference); assert languageCode != null; - if (languageCode.equals("")) { - // If current language code is empty, means none selected by user yet so use phone local - descriptionSecondaryLanguageListPreference.setSummary(Locale.getDefault().getDisplayLanguage()); - } else { - // If any language is selected by user previously, use it - Locale defLocale = createLocale(languageCode); - descriptionSecondaryLanguageListPreference.setSummary(defLocale.getDisplayLanguage(defLocale)); - } + descriptionSecondaryLanguageListPreference.setSummary("List additional languages."); + descriptionSecondaryLanguageListPreference.setOnPreferenceClickListener(new OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { - System.out.println("clickedseco"); - prepareAppLanguages(descriptionSecondaryLanguageListPreference.getKey()); + prepareSecondaryLanguageDialog(); return true; } }); @@ -292,6 +290,157 @@ public class SettingsFragment extends PreferenceFragmentCompat { }); } + private void updateSavedLanguages(ListView savedLanguageListView, List savedLanguages, HashMap selectedLanguages) { + // Use SavedLanguagesAdapter to display saved languages + SavedLanguagesAdapter savedLanguagesAdapter = new SavedLanguagesAdapter( + getActivity(), + savedLanguages, // List of saved Language objects + selectedLanguages // Pass the map of selected languages + ); + + // Set the adapter to the ListView to display the saved languages + savedLanguageListView.setAdapter(savedLanguagesAdapter); + } + + private ArrayList deSerialise(String languageCodes) { + // Check if the stored string is empty or null + if (languageCodes == null || languageCodes.isEmpty()) { + return new ArrayList<>(); // Return an empty list if there's no data + } + + // Split the string by commas and store it in a list + String[] languageArray = languageCodes.split(",\\s*"); // Split by comma and optional space + return new ArrayList<>(Arrays.asList(languageArray)); // Convert array to ArrayList and return + } + + + private void prepareSecondaryLanguageDialog() { + final String languageCode = getCurrentLanguageCode("descriptionSecondaryLanguagePref"); + HashMap selectedLanguages = new HashMap<>(); + assert languageCode != null; + selectedLanguages.put(0, Locale.getDefault().getLanguage()); + System.out.println(Locale.getDefault().getLanguage()); + System.out.println(languageCode); + + // Deserializing saved language codes to Language objects + ArrayList savedLanguages = new ArrayList<>(); + for (String code : deSerialise(languageCode)) { + System.out.println(code); + if(code.equals(Locale.getDefault().getLanguage())){ + System.out.println("match"); + continue; + } + Locale locale = new Locale(code); + savedLanguages.add(new Language(locale.getDisplayLanguage(locale), code)); + } + + // Create the new dialog for secondary language + Dialog dialog = new Dialog(getActivity()); + dialog.setContentView(R.layout.dialog_select_secondary_language); + dialog.setCanceledOnTouchOutside(true); + dialog.getWindow().setLayout( + (int) (getActivity().getResources().getDisplayMetrics().widthPixels * 0.90), + (int) (getActivity().getResources().getDisplayMetrics().heightPixels * 0.90) + ); + dialog.show(); + + // Bind UI elements + EditText editText = dialog.findViewById(R.id.search_language); + ListView listView = dialog.findViewById(R.id.language_list); + ListView savedLanguageListView = dialog.findViewById(R.id.language_history_list); + View separator = dialog.findViewById(R.id.separator); + + // Setup saved languages with the new SavedLanguagesAdapter + updateSavedLanguages(savedLanguageListView, savedLanguages, selectedLanguages); + + // Set an onItemClickListener to remove a language when clicked + savedLanguageListView.setOnItemClickListener((adapterView, view, position, id) -> { + // Remove the clicked language from Saved_Languages + savedLanguages.remove(position); + + // Update the saved language list view after removing the language + updateSavedLanguages(savedLanguageListView, savedLanguages, selectedLanguages); + + // Update the shared preferences to reflect the removal + String updatedLanguageCodes = ""; + for (Language language : savedLanguages) { + updatedLanguageCodes += language.getLanguageCode() + ", "; + } + // Remove the trailing comma and space if present + if (!updatedLanguageCodes.isEmpty()) { + updatedLanguageCodes = updatedLanguageCodes.substring(0, updatedLanguageCodes.length() - 2); + } + saveLanguageValue(updatedLanguageCodes, "descriptionSecondaryLanguagePref"); +// descriptionSecondaryLanguageListPreference.setSummary(getCurrentLanguageCode("descriptionSecondaryLanguagePref")); + }); + + // Set up the adapter for new languages using the selectedLanguages map + LanguagesAdapter languagesAdapter = new LanguagesAdapter(getActivity(), selectedLanguages); + listView.setAdapter(languagesAdapter); + + // Add search functionality + editText.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { + hideRecentLanguagesSection(); + } + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { + languagesAdapter.getFilter().filter(charSequence); + } + + @Override + public void afterTextChanged(Editable editable) { + } + }); + + // Handle item click for language selection in the main list + listView.setOnItemClickListener((adapterView, view, i, l) -> { + String selectedLanguageCode = languagesAdapter.getLanguageCode(i); + String selectedLanguageName = languagesAdapter.getLanguageName(i); + + if (deSerialise(getCurrentLanguageCode("descriptionSecondaryLanguagePref")).contains(selectedLanguageCode)) { + Toast.makeText(getActivity(), "Language already selected", Toast.LENGTH_SHORT).show(); + return; + } + + savedLanguages.add(new Language(selectedLanguageName, selectedLanguageCode)); + updateSavedLanguages(savedLanguageListView, savedLanguages, selectedLanguages); + + // Update the shared preferences to reflect the addition + String updatedLanguageCodes = ""; + for (Language language : savedLanguages) { + updatedLanguageCodes += language.getLanguageCode() + ", "; + } + // Remove the trailing comma and space if present + if (!updatedLanguageCodes.isEmpty()) { + updatedLanguageCodes = updatedLanguageCodes.substring(0, updatedLanguageCodes.length() - 2); + } + + saveLanguageValue(updatedLanguageCodes, "descriptionSecondaryLanguagePref"); + +// descriptionSecondaryLanguageListPreference.setSummary(getCurrentLanguageCode("descriptionSecondaryLanguagePref")); + }); + + dialog.setOnDismissListener(dialogInterface -> { + // Update the shared preferences to reflect changes + String updatedLanguageCodes = ""; + for (Language language : savedLanguages) { + updatedLanguageCodes += language.getLanguageCode() + ", "; + } + // Remove the trailing comma and space if present + if (!updatedLanguageCodes.isEmpty()) { + updatedLanguageCodes = updatedLanguageCodes.substring(0, updatedLanguageCodes.length() - 2); + } + saveLanguageValue(updatedLanguageCodes, "descriptionSecondaryLanguagePref"); + + }); + } + + + + /** * Prepare and Show language selection dialog box * Uses previously saved language if there is any, if not uses phone locale as initial language. @@ -303,7 +452,6 @@ public class SettingsFragment extends PreferenceFragmentCompat { */ private void prepareAppLanguages(final String keyListPreference) { - System.out.println("gets to prepare app languages"); // Gets current language code from shared preferences final String languageCode = getCurrentLanguageCode(keyListPreference); @@ -345,6 +493,7 @@ public class SettingsFragment extends PreferenceFragmentCompat { Dialog dialog = new Dialog(getActivity()); dialog.setContentView(R.layout.dialog_select_language); + dialog.setCanceledOnTouchOutside(true); dialog.getWindow().setLayout((int)(getActivity().getResources().getDisplayMetrics().widthPixels*0.90), (int)(getActivity().getResources().getDisplayMetrics().heightPixels*0.90)); @@ -407,9 +556,6 @@ public class SettingsFragment extends PreferenceFragmentCompat { }else if(keyListPreference.equals("descriptionDefaultLanguagePref")){ descriptionLanguageListPreference.setSummary(defLocale.getDisplayLanguage(defLocale)); } - else{ - descriptionSecondaryLanguageListPreference.setSummary(defLocale.getDisplayLanguage(defLocale)); - } dialog.dismiss(); } }); @@ -487,13 +633,43 @@ public class SettingsFragment extends PreferenceFragmentCompat { separator.setVisibility(View.GONE); } + private String reSerialise(ArrayList languageCodes) { + // Join the elements of the list into a single string, separated by a comma and a space + return String.join(", ", languageCodes); + } + /** * Changing the default app language with selected one and save it to SharedPreferences */ public void setLocale(final Activity activity, String userSelectedValue) { - if (userSelectedValue.equals("")) { - userSelectedValue = Locale.getDefault().getLanguage(); - } +// if (userSelectedValue.equals("")) { +// userSelectedValue = Locale.getDefault().getLanguage(); +// } +// +// String current = Locale.getDefault().getLanguage(); +// ArrayList languageCodes = deSerialise(current); +// if(appUI) { +// languageCodes.set(0, userSelectedValue); +// userSelectedValue = reSerialise(languageCodes); +// } +// else{ +// ArrayList newLanguageCodes = new ArrayList<>(); +// ArrayList userSelctedCode = deSerialise(userSelectedValue); +// +// newLanguageCodes.add(languageCodes.get(0)); +// for(String code : userSelctedCode){ +// newLanguageCodes.add(code); +// } +// userSelectedValue = reSerialise(newLanguageCodes); +// } +// +// System.out.println("Final locale"); +// System.out.println(userSelectedValue); +// +// System.out.println("vs"); +// System.out.println(getCurrentLanguageCode("appUiDefaultLanguagePref")); +// System.out.println(getCurrentLanguageCode("descriptionSecondaryLanguagePref")); + final Locale locale = createLocale(userSelectedValue); Locale.setDefault(locale); final Configuration configuration = new Configuration(); diff --git a/app/src/main/res/layout/dialog_select_secondary_language.xml b/app/src/main/res/layout/dialog_select_secondary_language.xml new file mode 100644 index 000000000..06de2b01f --- /dev/null +++ b/app/src/main/res/layout/dialog_select_secondary_language.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index c09853373..7c105feab 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -25,6 +25,17 @@ @string/exif_tag_name_serialNumbers @string/exif_tag_name_software + + + @string/exif_tag_name_author + @string/exif_tag_name_copyright + @string/exif_tag_name_location + @string/exif_tag_name_cameraModel + @string/exif_tag_name_lensModel + @string/exif_tag_name_serialNumbers + @string/exif_tag_name_software + + @string/exif_tag_author @string/exif_tag_copyright diff --git a/app/src/main/resources/queries/query_for_item.rq b/app/src/main/resources/queries/query_for_item.rq index bf6a1c09c..563f3d084 100644 --- a/app/src/main/resources/queries/query_for_item.rq +++ b/app/src/main/resources/queries/query_for_item.rq @@ -18,13 +18,14 @@ WHERE { } # Get the label in the preferred language of the user, or any other language if no label is available in that language. - OPTIONAL {?item rdfs:label ?itemLabelPreferredLanguage. FILTER (lang(?itemLabelPreferredLanguage) = "en")} + OPTIONAL {?item rdfs:label ?itemLabelPreferredLanguage. FILTER (lang(?itemLabelPreferredLanguage) = "${LANG}")} + ${SECONDARYLABEL} OPTIONAL {?item rdfs:label ?itemLabelAnyLanguage} BIND(COALESCE(?itemLabelPreferredLanguage, ?itemLabelAnyLanguage, "?") as ?label) # Get the description in the preferred language of the user, or any other language if no description is available in that language. - # This ensures that the query first tries to fetch the label in the user's preferred language, and if it's unavailable, it falls back to English - OPTIONAL {?item schema:description ?itemDescriptionPreferredLanguage. FILTER (LANG(?itemDescriptionPreferredLanguage) = "${LANG}" || LANG(?itemDescriptionPreferredLanguage) = "en")} + OPTIONAL {?item schema:description ?itemDescriptionPreferredLanguage. FILTER (lang(?itemDescriptionPreferredLanguage) = "${LANG}")} + ${SECONDARYDESCRIPTION} OPTIONAL {?item schema:description ?itemDescriptionAnyLanguage} BIND(COALESCE(?itemDescriptionPreferredLanguage, ?itemDescriptionAnyLanguage, "?") as ?description) @@ -32,6 +33,7 @@ WHERE { OPTIONAL { ?item p:P31/ps:P31 ?class. OPTIONAL {?class rdfs:label ?classLabelPreferredLanguage. FILTER (lang(?classLabelPreferredLanguage) = "${LANG}")} + ${SECONDARYCLASSLABEL} OPTIONAL {?class rdfs:label ?classLabelAnyLanguage} BIND(COALESCE(?classLabelPreferredLanguage, ?classLabelAnyLanguage, "?") as ?classLabel) }