Skip to content

Commit

Permalink
initial version
Browse files Browse the repository at this point in the history
  • Loading branch information
ivictbor committed Jan 9, 2025
1 parent 4d7dc9e commit 9a1fe9d
Show file tree
Hide file tree
Showing 13 changed files with 2,678 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
custom/node_modules
dist
71 changes: 71 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.0.23] - next

### Improved

- Better instructions for LLM pluralization Slavik messages

### Added
- support for pluralization in Backend `tr` function
- add handy languagesList function to get all languages for translation of side apps

### Fixed
- invalidation of indvidual tr messages when translation using LLM adapter

## [1.0.22]

### Fixed

- More predictable class name
- When loading unique translations from category, they were not registered if existing in other category


## [1.0.21] - 2024-12-30

### Fixed
- improve cache reset when editing messages manually

### Added
- Translating external app" feature by using feedCategoryTranslations

## [1.0.20]

### Fixed
- fix automatic translations

## [1.0.14]

### Fixed

- Add `ignoreInitial` for watch to prevent initial messages loading
- Add locking mechanism to prevent initial messages loading call in parallel (just in case)

## [1.0.13]

- Deduplicate frontend strings before creating translations


## [1.0.12]

### Fixed

- live mode frontend translations loading when tmp dir is nopt preserver (e.g. docker cached /tmp pipeline)

## [1.0.11]

### Fixed

- cache invalidations on delete

## [v1.0.10]

### Fixed

- fix automatic translations for duplicate strings
- improve slavik pluralization generations by splitting the requests
102 changes: 102 additions & 0 deletions custom/LanguageInUserMenu.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<template>
<div class="min-w-40">
<div class="cursor-pointer flex items-center gap-1 block px-4 py-2 text-sm text-black
hover:bg-html dark:text-darkSidebarTextHover dark:hover:bg-darkSidebarItemHover dark:hover:text-darkSidebarTextActive
w-full select-none "
:class="{ 'bg-black bg-opacity-10 ': showDropdown }"
@click="showDropdown = !showDropdown"
>
<span class="mr-1">
<span class="flag-icon"
:class="`flag-icon-${getCountryCodeFromLangCode(selectedOption.value)}`"
></span>
</span>
<span>{{ selectedOption.label }}</span>

<IconCaretDownSolid class="h-5 w-5 text-lightPrimary dark:text-gray-400 opacity-50 transition duration-150 ease-in"
:class="{ 'transform rotate-180': showDropdown }"
/>
</div>

<div v-if="showDropdown" >

<div class="cursor-pointer flex items-center gap-1 block px-4 py-1 text-sm
text-black dark:text-darkSidebarTextHover
bg-black bg-opacity-10
hover:brightness-110
hover:text-lightPrimary dark:hover:text-darkPrimary
hover:bg-lightPrimaryContrast dark:hover:bg-darkPrimaryContrast
w-full text-select-none pl-5 select-none"
v-for="option in options.filter((opt) => opt.value !== selectedOption.value)"
@click="doChangeLang(option.value)"
>
<span class="mr-1">
<span class="flag-icon"
:class="`flag-icon-${getCountryCodeFromLangCode(option.value)}`"
></span>
</span>
<span>{{ option.label }}</span>

</div>
</div>


</div>
</template>

<script setup>
import 'flag-icon-css/css/flag-icons.min.css';
import { IconCaretDownSolid } from '@iconify-prerendered/vue-flowbite';
import { setLang, getCountryCodeFromLangCode, getLocalLang, setLocalLang } from './langCommon';
import { computed, ref, onMounted, watch } from 'vue';
import { useI18n } from 'vue-i18n';
const { setLocaleMessage, locale } = useI18n();
const showDropdown = ref(false);
const props = defineProps(['meta', 'resource']);
const selectedLanguage = ref('');
function doChangeLang(lang) {
setLocalLang(lang);
// unfortunately, we need this to recall all APIs
document.location.reload();
}
const options = computed(() => {
return props.meta.supportedLanguages.map((lang) => {
return {
value: lang.code,
label: lang.name,
};
});
});
const selectedOption = computed(() => {
const val = options.value.find((option) => option.value === selectedLanguage.value);
if (val) {
return val;
}
return options.value[0];
});
onMounted(() => {
console.log('Language In user menu mounted', props.meta.supportedLanguages);
selectedLanguage.value = getLocalLang(props.meta.supportedLanguages);
setLang({ setLocaleMessage, locale }, props.meta.pluginInstanceId, selectedLanguage.value);
// todo this mounted executed only on this component mount, f5 from another page apart login will not read it
});
</script>
76 changes: 76 additions & 0 deletions custom/LanguageUnderLogin.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<template>
<p class="text-gray-500 dark:text-gray-400 font-sm text-left mt-3 flex items-center justify-center">
<Select
class="w-full"
v-model="selectedLanguage"
:options="options"
:placeholder="$t('Select language')"
>
<template #item="{ option }">
<span class="mr-1">
<span class="flag-icon"
:class="`flag-icon-${getCountryCodeFromLangCode(option.value)}`"
></span>

</span>
<span>{{ option.label }}</span>
</template>

<template #selected-item="{option}">
<span class="mr-1">
<span class="flag-icon"
:class="`flag-icon-${getCountryCodeFromLangCode(option.value)}`"
></span>
</span>
<span>{{ option.label }}</span>
</template>
</Select>
</p>
</template>

<script setup>
import Select from '@/afcl/Select.vue';
import 'flag-icon-css/css/flag-icons.min.css';
import { setLang, getCountryCodeFromLangCode, getLocalLang } from './langCommon';
import { useCoreStore } from '@/stores/core';
import { computed, ref, onMounted, watch } from 'vue';
import { useI18n } from 'vue-i18n';
const { setLocaleMessage, locale } = useI18n();
const props = defineProps(['meta', 'resource']);
const selectedLanguage = ref('');
const coreStore = useCoreStore();
watch(() => selectedLanguage.value, async (newVal) => {
await setLang({ setLocaleMessage, locale }, props.meta.pluginInstanceId, newVal);
coreStore.getPublicConfig();
});
const options = computed(() => {
return props.meta.supportedLanguages.map((lang) => {
return {
value: lang.code,
label: lang.name,
};
});
});
onMounted(() => {
console.log('LanguageUnderLogin mounted', props.meta.supportedLanguages);
selectedLanguage.value = getLocalLang(props.meta.supportedLanguages);
setLang({ setLocaleMessage, locale }, props.meta.pluginInstanceId, selectedLanguage.value);
// todo this mounted executed only on this component mount, f5 from another page apart login will not read it
});
</script>
89 changes: 89 additions & 0 deletions custom/langCommon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@

import { callAdminForthApi } from '@/utils';


const messagesCache: Record<
string,
{
ts: number;
messages: Record<string, string>;
}
> = {};

// cleanup messages after a 2 minutes (cache for instant switching)
setInterval(() => {
const now = Date.now();
for (const lang in messagesCache) {
if (now - messagesCache[lang].ts > 10 * 60 * 1000) {
delete messagesCache[lang];
}
}
}, 60 * 1000);

// i18n is vue-i18n instance
export async function setLang({ setLocaleMessage, locale }: any, pluginInstanceId: string, langIso: string) {

if (!messagesCache[langIso]) {
const messages = await callAdminForthApi({
path: `/plugin/${pluginInstanceId}/frontend_messages?lang=${langIso}`,
method: 'GET',
});
messagesCache[langIso] = {
ts: Date.now(),
messages: messages
};
}

// set locale and locale message
setLocaleMessage(langIso, messagesCache[langIso].messages);

// set the language
locale.value = langIso;

document.querySelector('html').setAttribute('lang', langIso);
setLocalLang(langIso);
}

// only remap the country code for the languages where language code is different from the country code
// don't include es: es, fr: fr, etc, only include the ones where language code is different from the country code
const countryISO31661ByLangISO6391 = {
en: 'us', // English → United States
zh: 'cn', // Chinese → China
hi: 'in', // Hindi → India
ar: 'sa', // Arabic → Saudi Arabia
ko: 'kr', // Korean → South Korea
ja: 'jp', // Japanese → Japan
uk: 'ua', // Ukrainian → Ukraine
ur: 'pk', // Urdu → Pakistan
};

export function getCountryCodeFromLangCode(langCode) {
return countryISO31661ByLangISO6391[langCode] || langCode;
}


const LS_LANG_KEY = `afLanguage`;

export function getLocalLang(supportedLanguages: {code}[]): string {
let lsLang = localStorage.getItem(LS_LANG_KEY);
// if someone screwed up the local storage or we stopped language support, lets check if it is in supported languages
if (lsLang && !supportedLanguages.find((l) => l.code == lsLang)) {
lsLang = null;
}
if (lsLang) {
return lsLang;
}
// read lang from navigator and try find what we have in supported languages
const lang = navigator.language.split('-')[0];
const foundLang = supportedLanguages.find((l) => l.code == lang);
if (foundLang) {
return foundLang.code;
}
return supportedLanguages[0].code;
}

export function setLocalLang(lang: string) {
localStorage.setItem(LS_LANG_KEY, lang);
}


24 changes: 24 additions & 0 deletions custom/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions custom/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "custom",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"devDependencies": {
"flag-icon-css": "^4.1.7"
}
}
Loading

0 comments on commit 9a1fe9d

Please sign in to comment.