Skip to content

Commit 9a1fe9d

Browse files
committed
initial version
1 parent 4d7dc9e commit 9a1fe9d

13 files changed

Lines changed: 2678 additions & 0 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules
2+
custom/node_modules
3+
dist

Changelog.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [1.0.23] - next
9+
10+
### Improved
11+
12+
- Better instructions for LLM pluralization Slavik messages
13+
14+
### Added
15+
- support for pluralization in Backend `tr` function
16+
- add handy languagesList function to get all languages for translation of side apps
17+
18+
### Fixed
19+
- invalidation of indvidual tr messages when translation using LLM adapter
20+
21+
## [1.0.22]
22+
23+
### Fixed
24+
25+
- More predictable class name
26+
- When loading unique translations from category, they were not registered if existing in other category
27+
28+
29+
## [1.0.21] - 2024-12-30
30+
31+
### Fixed
32+
- improve cache reset when editing messages manually
33+
34+
### Added
35+
- Translating external app" feature by using feedCategoryTranslations
36+
37+
## [1.0.20]
38+
39+
### Fixed
40+
- fix automatic translations
41+
42+
## [1.0.14]
43+
44+
### Fixed
45+
46+
- Add `ignoreInitial` for watch to prevent initial messages loading
47+
- Add locking mechanism to prevent initial messages loading call in parallel (just in case)
48+
49+
## [1.0.13]
50+
51+
- Deduplicate frontend strings before creating translations
52+
53+
54+
## [1.0.12]
55+
56+
### Fixed
57+
58+
- live mode frontend translations loading when tmp dir is nopt preserver (e.g. docker cached /tmp pipeline)
59+
60+
## [1.0.11]
61+
62+
### Fixed
63+
64+
- cache invalidations on delete
65+
66+
## [v1.0.10]
67+
68+
### Fixed
69+
70+
- fix automatic translations for duplicate strings
71+
- improve slavik pluralization generations by splitting the requests

custom/LanguageInUserMenu.vue

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<template>
2+
<div class="min-w-40">
3+
<div class="cursor-pointer flex items-center gap-1 block px-4 py-2 text-sm text-black
4+
hover:bg-html dark:text-darkSidebarTextHover dark:hover:bg-darkSidebarItemHover dark:hover:text-darkSidebarTextActive
5+
w-full select-none "
6+
:class="{ 'bg-black bg-opacity-10 ': showDropdown }"
7+
@click="showDropdown = !showDropdown"
8+
>
9+
<span class="mr-1">
10+
<span class="flag-icon"
11+
:class="`flag-icon-${getCountryCodeFromLangCode(selectedOption.value)}`"
12+
></span>
13+
</span>
14+
<span>{{ selectedOption.label }}</span>
15+
16+
<IconCaretDownSolid class="h-5 w-5 text-lightPrimary dark:text-gray-400 opacity-50 transition duration-150 ease-in"
17+
:class="{ 'transform rotate-180': showDropdown }"
18+
/>
19+
</div>
20+
21+
<div v-if="showDropdown" >
22+
23+
<div class="cursor-pointer flex items-center gap-1 block px-4 py-1 text-sm
24+
text-black dark:text-darkSidebarTextHover
25+
bg-black bg-opacity-10
26+
hover:brightness-110
27+
hover:text-lightPrimary dark:hover:text-darkPrimary
28+
hover:bg-lightPrimaryContrast dark:hover:bg-darkPrimaryContrast
29+
w-full text-select-none pl-5 select-none"
30+
v-for="option in options.filter((opt) => opt.value !== selectedOption.value)"
31+
@click="doChangeLang(option.value)"
32+
>
33+
<span class="mr-1">
34+
<span class="flag-icon"
35+
:class="`flag-icon-${getCountryCodeFromLangCode(option.value)}`"
36+
></span>
37+
</span>
38+
<span>{{ option.label }}</span>
39+
40+
</div>
41+
</div>
42+
43+
44+
</div>
45+
</template>
46+
47+
<script setup>
48+
import 'flag-icon-css/css/flag-icons.min.css';
49+
import { IconCaretDownSolid } from '@iconify-prerendered/vue-flowbite';
50+
51+
import { setLang, getCountryCodeFromLangCode, getLocalLang, setLocalLang } from './langCommon';
52+
53+
import { computed, ref, onMounted, watch } from 'vue';
54+
import { useI18n } from 'vue-i18n';
55+
56+
const { setLocaleMessage, locale } = useI18n();
57+
58+
const showDropdown = ref(false);
59+
const props = defineProps(['meta', 'resource']);
60+
61+
const selectedLanguage = ref('');
62+
63+
function doChangeLang(lang) {
64+
setLocalLang(lang);
65+
// unfortunately, we need this to recall all APIs
66+
document.location.reload();
67+
68+
}
69+
70+
71+
const options = computed(() => {
72+
return props.meta.supportedLanguages.map((lang) => {
73+
return {
74+
value: lang.code,
75+
label: lang.name,
76+
};
77+
});
78+
});
79+
80+
const selectedOption = computed(() => {
81+
const val = options.value.find((option) => option.value === selectedLanguage.value);
82+
if (val) {
83+
return val;
84+
}
85+
return options.value[0];
86+
});
87+
88+
89+
onMounted(() => {
90+
console.log('Language In user menu mounted', props.meta.supportedLanguages);
91+
selectedLanguage.value = getLocalLang(props.meta.supportedLanguages);
92+
setLang({ setLocaleMessage, locale }, props.meta.pluginInstanceId, selectedLanguage.value);
93+
// todo this mounted executed only on this component mount, f5 from another page apart login will not read it
94+
});
95+
96+
97+
98+
99+
100+
101+
102+
</script>

custom/LanguageUnderLogin.vue

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<template>
2+
<p class="text-gray-500 dark:text-gray-400 font-sm text-left mt-3 flex items-center justify-center">
3+
<Select
4+
class="w-full"
5+
v-model="selectedLanguage"
6+
:options="options"
7+
:placeholder="$t('Select language')"
8+
>
9+
<template #item="{ option }">
10+
<span class="mr-1">
11+
<span class="flag-icon"
12+
:class="`flag-icon-${getCountryCodeFromLangCode(option.value)}`"
13+
></span>
14+
15+
</span>
16+
<span>{{ option.label }}</span>
17+
</template>
18+
19+
<template #selected-item="{option}">
20+
<span class="mr-1">
21+
<span class="flag-icon"
22+
:class="`flag-icon-${getCountryCodeFromLangCode(option.value)}`"
23+
></span>
24+
</span>
25+
<span>{{ option.label }}</span>
26+
</template>
27+
</Select>
28+
</p>
29+
</template>
30+
31+
<script setup>
32+
import Select from '@/afcl/Select.vue';
33+
import 'flag-icon-css/css/flag-icons.min.css';
34+
import { setLang, getCountryCodeFromLangCode, getLocalLang } from './langCommon';
35+
import { useCoreStore } from '@/stores/core';
36+
37+
import { computed, ref, onMounted, watch } from 'vue';
38+
import { useI18n } from 'vue-i18n';
39+
40+
const { setLocaleMessage, locale } = useI18n();
41+
42+
43+
const props = defineProps(['meta', 'resource']);
44+
45+
const selectedLanguage = ref('');
46+
const coreStore = useCoreStore();
47+
48+
watch(() => selectedLanguage.value, async (newVal) => {
49+
await setLang({ setLocaleMessage, locale }, props.meta.pluginInstanceId, newVal);
50+
coreStore.getPublicConfig();
51+
});
52+
53+
54+
const options = computed(() => {
55+
return props.meta.supportedLanguages.map((lang) => {
56+
return {
57+
value: lang.code,
58+
label: lang.name,
59+
};
60+
});
61+
});
62+
63+
onMounted(() => {
64+
console.log('LanguageUnderLogin mounted', props.meta.supportedLanguages);
65+
selectedLanguage.value = getLocalLang(props.meta.supportedLanguages);
66+
setLang({ setLocaleMessage, locale }, props.meta.pluginInstanceId, selectedLanguage.value);
67+
// todo this mounted executed only on this component mount, f5 from another page apart login will not read it
68+
});
69+
70+
71+
72+
73+
74+
75+
76+
</script>

custom/langCommon.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
2+
import { callAdminForthApi } from '@/utils';
3+
4+
5+
const messagesCache: Record<
6+
string,
7+
{
8+
ts: number;
9+
messages: Record<string, string>;
10+
}
11+
> = {};
12+
13+
// cleanup messages after a 2 minutes (cache for instant switching)
14+
setInterval(() => {
15+
const now = Date.now();
16+
for (const lang in messagesCache) {
17+
if (now - messagesCache[lang].ts > 10 * 60 * 1000) {
18+
delete messagesCache[lang];
19+
}
20+
}
21+
}, 60 * 1000);
22+
23+
// i18n is vue-i18n instance
24+
export async function setLang({ setLocaleMessage, locale }: any, pluginInstanceId: string, langIso: string) {
25+
26+
if (!messagesCache[langIso]) {
27+
const messages = await callAdminForthApi({
28+
path: `/plugin/${pluginInstanceId}/frontend_messages?lang=${langIso}`,
29+
method: 'GET',
30+
});
31+
messagesCache[langIso] = {
32+
ts: Date.now(),
33+
messages: messages
34+
};
35+
}
36+
37+
// set locale and locale message
38+
setLocaleMessage(langIso, messagesCache[langIso].messages);
39+
40+
// set the language
41+
locale.value = langIso;
42+
43+
document.querySelector('html').setAttribute('lang', langIso);
44+
setLocalLang(langIso);
45+
}
46+
47+
// only remap the country code for the languages where language code is different from the country code
48+
// don't include es: es, fr: fr, etc, only include the ones where language code is different from the country code
49+
const countryISO31661ByLangISO6391 = {
50+
en: 'us', // English → United States
51+
zh: 'cn', // Chinese → China
52+
hi: 'in', // Hindi → India
53+
ar: 'sa', // Arabic → Saudi Arabia
54+
ko: 'kr', // Korean → South Korea
55+
ja: 'jp', // Japanese → Japan
56+
uk: 'ua', // Ukrainian → Ukraine
57+
ur: 'pk', // Urdu → Pakistan
58+
};
59+
60+
export function getCountryCodeFromLangCode(langCode) {
61+
return countryISO31661ByLangISO6391[langCode] || langCode;
62+
}
63+
64+
65+
const LS_LANG_KEY = `afLanguage`;
66+
67+
export function getLocalLang(supportedLanguages: {code}[]): string {
68+
let lsLang = localStorage.getItem(LS_LANG_KEY);
69+
// if someone screwed up the local storage or we stopped language support, lets check if it is in supported languages
70+
if (lsLang && !supportedLanguages.find((l) => l.code == lsLang)) {
71+
lsLang = null;
72+
}
73+
if (lsLang) {
74+
return lsLang;
75+
}
76+
// read lang from navigator and try find what we have in supported languages
77+
const lang = navigator.language.split('-')[0];
78+
const foundLang = supportedLanguages.find((l) => l.code == lang);
79+
if (foundLang) {
80+
return foundLang.code;
81+
}
82+
return supportedLanguages[0].code;
83+
}
84+
85+
export function setLocalLang(lang: string) {
86+
localStorage.setItem(LS_LANG_KEY, lang);
87+
}
88+
89+

custom/package-lock.json

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

custom/package.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "custom",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"scripts": {
6+
"test": "echo \"Error: no test specified\" && exit 1"
7+
},
8+
"keywords": [],
9+
"author": "",
10+
"license": "ISC",
11+
"description": "",
12+
"devDependencies": {
13+
"flag-icon-css": "^4.1.7"
14+
}
15+
}

0 commit comments

Comments
 (0)