VBV-746: Neuer technischer Prozess Übersetzungen
This commit is contained in:
parent
31fc9f46aa
commit
4274d47207
65
README.md
65
README.md
|
|
@ -101,53 +101,38 @@ Preferences -> Tools -> Actions on Save
|
||||||
|
|
||||||
## Translations
|
## Translations
|
||||||
|
|
||||||
We use (Locize)[https://locize.com] (see 1Password for credentials)
|
We use the [i18next](https://www.i18next.com/) library for translations.
|
||||||
together with (i18next)[https://www.i18next.com/]
|
Please add `a.` prefix for new translation keys in the code:
|
||||||
for translations on the Frontend.
|
|
||||||
|
|
||||||
Please make sure that the required environment variables are set
|
```
|
||||||
(see ./env_secrets/local_daniel.env for the values):
|
<h4>{{ $t("a.Demo mit Daniel") }}</h4>
|
||||||
|
|
||||||
* LOCIZE_PROJECT_ID
|
|
||||||
* LOCIZE_API_KEY
|
|
||||||
|
|
||||||
The master for translated files is on Locize!
|
|
||||||
That means, that the app will take the translations/texts from Locize
|
|
||||||
to show in the app.
|
|
||||||
The files in ./client/locales are only used as reference and are not the master!
|
|
||||||
|
|
||||||
There are multiple ways on how to add new translations to Locize:
|
|
||||||
|
|
||||||
### Process one: Let Locize add missing keys automatically
|
|
||||||
|
|
||||||
When running the app, it will automatically add the missing translation
|
|
||||||
keys to Locize.
|
|
||||||
There you can translate them, and also add the German translation.
|
|
||||||
|
|
||||||
### Process two: Add keys manually
|
|
||||||
|
|
||||||
You can add the new keys manually to the German locale file in
|
|
||||||
./client/locales/de/translation.json
|
|
||||||
|
|
||||||
Then you can run the following command to add the keys to Locize:
|
|
||||||
|
|
||||||
### Helpers
|
|
||||||
|
|
||||||
The following command could help find missing and/or unused keys.
|
|
||||||
But manual review is still needed.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx vue-i18n-extract report --vueFiles './src/**/*.?(ts|vue)' --languageFiles './src/locales/**/*.?(json|yml|yaml)'
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
With [i18next-parser](https://github.com/i18next/i18next-parser) we can extraxt the
|
||||||
|
new translation keys directly from the code to the language files in `./client/src/locales`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run locize:sync
|
# extract new translation keys
|
||||||
|
# in `./client` directory
|
||||||
|
npm run i18next:parse
|
||||||
```
|
```
|
||||||
|
|
||||||
The command will add the keys and the German translation to Locize.
|
The new keys will end up in the `./client/src/locales/de/translation.json` file.
|
||||||
|
|
||||||
|
You can translate the keys directly in the files.
|
||||||
|
|
||||||
|
#### Bonus: Translate with BabelEdit desktop app
|
||||||
|
|
||||||
|
The [BabelEdit](https://www.codeandweb.com/babeledit) desktop can help directly with
|
||||||
|
the translation.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Here are the settings that I use in BabelEdit:
|
||||||
|

|
||||||
|
|
||||||
|
Run the `npm run i18next:sort` to get a "clean" sorted file after edit with BabelEdit.
|
||||||
|
|
||||||
Bonus: Use the "i18n ally" plugin in VSCode or IntelliJ to get extract untranslated
|
|
||||||
texts directly from the code to the translation.json file.
|
|
||||||
|
|
||||||
### "_many" plural form in French and Italian
|
### "_many" plural form in French and Italian
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,3 +26,4 @@ coverage
|
||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
babel-edit.babel
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"htmlWhitespaceSensitivity": "ignore",
|
"htmlWhitespaceSensitivity": "ignore",
|
||||||
"jsonRecursiveSort": true,
|
"jsonRecursiveSort": true,
|
||||||
|
"jsonSortOrder": "{\"/.*/\": \"caseInsensitiveNumeric\"}",
|
||||||
"organizeImportsSkipDestructiveCodeActions": true,
|
"organizeImportsSkipDestructiveCodeActions": true,
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"prettier-plugin-sort-json",
|
"prettier-plugin-sort-json",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
// i18next-parser.config.js
|
||||||
|
|
||||||
|
export default {
|
||||||
|
contextSeparator: "_",
|
||||||
|
// Key separator used in your translation keys
|
||||||
|
|
||||||
|
createOldCatalogs: false,
|
||||||
|
// Save the \_old files
|
||||||
|
|
||||||
|
defaultNamespace: "translation",
|
||||||
|
// Default namespace used in your i18next config
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
defaultValue: function (locale, namespace, key, value) {
|
||||||
|
if (locale === "de") {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
// Default value to give to keys with no value
|
||||||
|
// You may also specify a function accepting the locale, namespace, key, and value as arguments
|
||||||
|
|
||||||
|
indentation: 2,
|
||||||
|
// Indentation of the catalog files
|
||||||
|
|
||||||
|
keepRemoved: true,
|
||||||
|
// Keep keys from the catalog that are no longer in code
|
||||||
|
// You may either specify a boolean to keep or discard all removed keys.
|
||||||
|
// You may also specify an array of patterns: the keys from the catalog that are no long in the code but match one of the patterns will be kept.
|
||||||
|
// The patterns are applied to the full key including the namespace, the parent keys and the separators.
|
||||||
|
|
||||||
|
keySeparator: false,
|
||||||
|
// Key separator used in your translation keys
|
||||||
|
// If you want to use plain english keys, separators such as `.` and `:` will conflict. You might want to set `keySeparator: false` and `namespaceSeparator: false`. That way, `t('Status: Loading...')` will not think that there are a namespace and three separator dots for instance.
|
||||||
|
|
||||||
|
// see below for more details
|
||||||
|
lexers: {
|
||||||
|
vue: [
|
||||||
|
{
|
||||||
|
lexer: "JavascriptLexer",
|
||||||
|
functions: ["t", "$t"], // Array of functions to match
|
||||||
|
namespaceFunctions: ["useTranslation", "withTranslation"], // Array of functions to match for namespace
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: ["JavascriptLexer"],
|
||||||
|
},
|
||||||
|
|
||||||
|
lineEnding: "auto",
|
||||||
|
// Control the line ending. See options at https://github.com/ryanve/eol
|
||||||
|
|
||||||
|
locales: ["de", "fr", "it"],
|
||||||
|
// An array of the locales in your applications
|
||||||
|
|
||||||
|
namespaceSeparator: false,
|
||||||
|
// Namespace separator used in your translation keys
|
||||||
|
// If you want to use plain english keys, separators such as `.` and `:` will conflict. You might want to set `keySeparator: false` and `namespaceSeparator: false`. That way, `t('Status: Loading...')` will not think that there are a namespace and three separator dots for instance.
|
||||||
|
|
||||||
|
output: "src/locales/$LOCALE/$NAMESPACE.json",
|
||||||
|
// Supports $LOCALE and $NAMESPACE injection
|
||||||
|
// Supports JSON (.json) and YAML (.yml) file formats
|
||||||
|
// Where to write the locale files relative to process.cwd()
|
||||||
|
|
||||||
|
pluralSeparator: "_",
|
||||||
|
// Plural separator used in your translation keys
|
||||||
|
// If you want to use plain english keys, separators such as `_` might conflict. You might want to set `pluralSeparator` to a different string that does not occur in your keys.
|
||||||
|
// If you don't want to generate keys for plurals (for example, in case you are using ICU format), set `pluralSeparator: false`.
|
||||||
|
|
||||||
|
input: ["src/**/*.{js,ts,vue}"],
|
||||||
|
// An array of globs that describe where to look for source files
|
||||||
|
// relative to the location of the configuration file
|
||||||
|
|
||||||
|
sort: false,
|
||||||
|
// Whether or not to sort the catalog. Can also be a [compareFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#parameters)
|
||||||
|
|
||||||
|
verbose: false,
|
||||||
|
// Display info about the parsing including some stats
|
||||||
|
|
||||||
|
failOnWarnings: false,
|
||||||
|
// Exit with an exit code of 1 on warnings
|
||||||
|
|
||||||
|
failOnUpdate: false,
|
||||||
|
// Exit with an exit code of 1 when translations are updated (for CI purpose)
|
||||||
|
|
||||||
|
customValueTemplate: null,
|
||||||
|
// If you wish to customize the value output the value as an object, you can set your own format.
|
||||||
|
//
|
||||||
|
// - ${defaultValue} is the default value you set in your translation function.
|
||||||
|
// - ${filePaths} will be expanded to an array that contains the absolute
|
||||||
|
// file paths where the translations originated in, in case e.g., you need
|
||||||
|
// to provide translators with context
|
||||||
|
//
|
||||||
|
// Any other custom property will be automatically extracted from the 2nd
|
||||||
|
// argument of your `t()` function or tOptions in <Trans tOptions={...} />
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
// For `t('my-key', {maxLength: 150, defaultValue: 'Hello'})` in
|
||||||
|
// /path/to/your/file.js,
|
||||||
|
//
|
||||||
|
// Using the following customValueTemplate:
|
||||||
|
//
|
||||||
|
// customValueTemplate: {
|
||||||
|
// message: "${defaultValue}",
|
||||||
|
// description: "${maxLength}",
|
||||||
|
// paths: "${filePaths}",
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Will result in the following item being extracted:
|
||||||
|
//
|
||||||
|
// "my-key": {
|
||||||
|
// "message": "Hello",
|
||||||
|
// "description": 150,
|
||||||
|
// "paths": ["/path/to/your/file.js"]
|
||||||
|
// }
|
||||||
|
|
||||||
|
resetDefaultValueLocale: null,
|
||||||
|
// The locale to compare with default values to determine whether a default value has been changed.
|
||||||
|
// If this is set and a default value differs from a translation in the specified locale, all entries
|
||||||
|
// for that key across locales are reset to the default value, and existing translations are moved to
|
||||||
|
// the `_old` file.
|
||||||
|
|
||||||
|
i18nextOptions: null,
|
||||||
|
// If you wish to customize options in internally used i18next instance, you can define an object with any
|
||||||
|
// configuration property supported by i18next (https://www.i18next.com/overview/configuration-options).
|
||||||
|
// { compatibilityJSON: 'v3' } can be used to generate v3 compatible plurals.
|
||||||
|
|
||||||
|
yamlOptions: null,
|
||||||
|
// If you wish to customize options for yaml output, you can define an object here.
|
||||||
|
// Configuration options are here (https://github.com/nodeca/js-yaml#dump-object---options-).
|
||||||
|
// Example:
|
||||||
|
// {
|
||||||
|
// lineWidth: -1,
|
||||||
|
// }
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -16,7 +16,9 @@
|
||||||
"tailwind": "tailwindcss -i tailwind.css -o ../server/vbv_lernwelt/static/css/tailwind.css --watch",
|
"tailwind": "tailwindcss -i tailwind.css -o ../server/vbv_lernwelt/static/css/tailwind.css --watch",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"typecheck": "npm run codegen && vue-tsc --noEmit -p tsconfig.app.json --composite false",
|
"typecheck": "npm run codegen && vue-tsc --noEmit -p tsconfig.app.json --composite false",
|
||||||
"typecheck-only": "vue-tsc --noEmit -p tsconfig.app.json --composite false"
|
"typecheck-only": "vue-tsc --noEmit -p tsconfig.app.json --composite false",
|
||||||
|
"i18next:sort": "prettier --write src/locales/**/*.json",
|
||||||
|
"i18next:parse": "i18next && npm run i18next:sort"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/tailwindcss": "^0.2.1",
|
"@headlessui/tailwindcss": "^0.2.1",
|
||||||
|
|
@ -72,6 +74,7 @@
|
||||||
"eslint-config-prettier": "^8.10.0",
|
"eslint-config-prettier": "^8.10.0",
|
||||||
"eslint-plugin-cypress": "^2.15.2",
|
"eslint-plugin-cypress": "^2.15.2",
|
||||||
"eslint-plugin-vue": "^9.27.0",
|
"eslint-plugin-vue": "^9.27.0",
|
||||||
|
"i18next-parser": "^9.0.2",
|
||||||
"jsdom": "^24.1.0",
|
"jsdom": "^24.1.0",
|
||||||
"locize-cli": "^8.0.1",
|
"locize-cli": "^8.0.1",
|
||||||
"postcss": "^8.4.39",
|
"postcss": "^8.4.39",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
import type { AvailableLanguages } from "@/stores/user";
|
import type { AvailableLanguages } from "@/stores/user";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import Backend from "i18next-locize-backend";
|
|
||||||
import { locizePlugin } from "locize";
|
|
||||||
|
|
||||||
import { nextTick } from "vue";
|
import { nextTick } from "vue";
|
||||||
|
|
||||||
|
|
@ -21,23 +19,42 @@ export function i18nextInit() {
|
||||||
// .use(LanguageDetector)
|
// .use(LanguageDetector)
|
||||||
// init i18next
|
// init i18next
|
||||||
// for all options read: https://www.i18next.com/overview/configuration-options
|
// for all options read: https://www.i18next.com/overview/configuration-options
|
||||||
.use(Backend)
|
// .use(Backend)
|
||||||
.use(locizePlugin)
|
// .use(locizePlugin)
|
||||||
.init({
|
.init({
|
||||||
debug: true,
|
debug: true,
|
||||||
supportedLngs: SUPPORT_LOCALES,
|
supportedLngs: SUPPORT_LOCALES,
|
||||||
fallbackLng: "de",
|
fallbackLng: "de",
|
||||||
defaultNS: "translation",
|
defaultNS: "translation",
|
||||||
returnNull: false,
|
returnNull: false,
|
||||||
saveMissing: import.meta.env.DEV,
|
saveMissing: false,
|
||||||
backend: {
|
keySeparator: false,
|
||||||
projectId:
|
resources: {
|
||||||
import.meta.env.VITE_LOCIZE_PROJECTID ||
|
de: {
|
||||||
"7518c269-cbf7-4d25-bc5c-6ceba2a8b74b",
|
translation: {
|
||||||
apiKey: import.meta.env.DEV ? import.meta.env.VITE_LOCIZE_API_KEY : undefined,
|
"general.title": "myVBV",
|
||||||
fallbackLng: "de",
|
},
|
||||||
allowedAddOrUpdateHosts: ["localhost", "127.0.0.1"],
|
},
|
||||||
|
fr: {
|
||||||
|
translation: {
|
||||||
|
"general.title": "myAFA",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
it: {
|
||||||
|
translation: {
|
||||||
|
"general.title": "myAFA",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// backend: {
|
||||||
|
// projectId:
|
||||||
|
// import.meta.env.VITE_LOCIZE_PROJECTID ||
|
||||||
|
// "7518c269-cbf7-4d25-bc5c-6ceba2a8b74b",
|
||||||
|
// apiKey: import.meta.env.DEV ? import.meta.env.VITE_LOCIZE_API_KEY : undefined,
|
||||||
|
// fallbackLng: "de",
|
||||||
|
// allowedAddOrUpdateHosts: ["localhost", "127.0.0.1"],
|
||||||
|
// },
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -56,10 +73,18 @@ export function setI18nLanguage(locale: string) {
|
||||||
|
|
||||||
export async function loadI18nextLocaleMessages(locale: any) {
|
export async function loadI18nextLocaleMessages(locale: any) {
|
||||||
// load locale messages with dynamic import
|
// load locale messages with dynamic import
|
||||||
// unused with locize
|
let messages = null;
|
||||||
const messages = await import(`./locales/${locale}.json`);
|
if (locale === "de") {
|
||||||
|
messages = await import("./locales/de/translation.json");
|
||||||
|
} else if (locale === "fr") {
|
||||||
|
messages = await import("./locales/fr/translation.json");
|
||||||
|
} else if (locale === "it") {
|
||||||
|
messages = await import("./locales/it/translation.json");
|
||||||
|
}
|
||||||
|
|
||||||
i18next.addResourceBundle(locale, "messages", messages, true, true);
|
if (messages) {
|
||||||
|
i18next.addResourceBundle(locale, "translation", messages.default, true, true);
|
||||||
|
}
|
||||||
|
|
||||||
return nextTick();
|
return nextTick();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -1,4 +1,4 @@
|
||||||
import { i18nextInit } from "@/i18nextWrapper";
|
import { i18nextInit, loadI18nextLocaleMessages } from "@/i18nextWrapper";
|
||||||
import { generateLocalSessionKey } from "@/statistics";
|
import { generateLocalSessionKey } from "@/statistics";
|
||||||
import * as Sentry from "@sentry/vue";
|
import * as Sentry from "@sentry/vue";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
@ -48,6 +48,8 @@ Sentry.init({
|
||||||
});
|
});
|
||||||
|
|
||||||
i18nextInit().then(() => {
|
i18nextInit().then(() => {
|
||||||
app.use(I18NextVue, { i18next });
|
loadI18nextLocaleMessages("de").then(() => {
|
||||||
app.mount("#app");
|
app.use(I18NextVue, { i18next });
|
||||||
|
app.mount("#app");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ function findUserPointsHtml(userId: string) {
|
||||||
|
|
||||||
if (!gradedUser.passed) {
|
if (!gradedUser.passed) {
|
||||||
result += ` <span class="my-2 rounded-md bg-error-red-200 px-2.5 py-0.5 inline-block leading-5">${t(
|
result += ` <span class="my-2 rounded-md bg-error-red-200 px-2.5 py-0.5 inline-block leading-5">${t(
|
||||||
"a.Nicht bestanden"
|
"a.Nicht Bestanden"
|
||||||
)}</span>`;
|
)}</span>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { bustItGetCache, itGetCached, itPost } from "@/fetchHelpers";
|
import { bustItGetCache, itGetCached, itPost } from "@/fetchHelpers";
|
||||||
import { setI18nLanguage } from "@/i18nextWrapper";
|
import { loadI18nextLocaleMessages, setI18nLanguage } from "@/i18nextWrapper";
|
||||||
import type { Country, CourseProfile } from "@/services/entities";
|
import type { Country, CourseProfile } from "@/services/entities";
|
||||||
import { directUpload } from "@/services/files";
|
import { directUpload } from "@/services/files";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
@ -106,6 +106,7 @@ async function setLocale(language: AvailableLanguages) {
|
||||||
}
|
}
|
||||||
dayjs.locale(language);
|
dayjs.locale(language);
|
||||||
setI18nLanguage(language);
|
setI18nLanguage(language);
|
||||||
|
loadI18nextLocaleMessages(language);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useUserStore = defineStore({
|
export const useUserStore = defineStore({
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 589 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 382 KiB |
Loading…
Reference in New Issue