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
|
||||
|
||||
We use (Locize)[https://locize.com] (see 1Password for credentials)
|
||||
together with (i18next)[https://www.i18next.com/]
|
||||
for translations on the Frontend.
|
||||
We use the [i18next](https://www.i18next.com/) library for translations.
|
||||
Please add `a.` prefix for new translation keys in the code:
|
||||
|
||||
Please make sure that the required environment variables are set
|
||||
(see ./env_secrets/local_daniel.env for the values):
|
||||
|
||||
* 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)'
|
||||
```
|
||||
<h4>{{ $t("a.Demo mit Daniel") }}</h4>
|
||||
```
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -26,3 +26,4 @@ coverage
|
|||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
babel-edit.babel
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"htmlWhitespaceSensitivity": "ignore",
|
||||
"jsonRecursiveSort": true,
|
||||
"jsonSortOrder": "{\"/.*/\": \"caseInsensitiveNumeric\"}",
|
||||
"organizeImportsSkipDestructiveCodeActions": true,
|
||||
"plugins": [
|
||||
"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",
|
||||
"test": "vitest run",
|
||||
"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": {
|
||||
"@headlessui/tailwindcss": "^0.2.1",
|
||||
|
|
@ -72,6 +74,7 @@
|
|||
"eslint-config-prettier": "^8.10.0",
|
||||
"eslint-plugin-cypress": "^2.15.2",
|
||||
"eslint-plugin-vue": "^9.27.0",
|
||||
"i18next-parser": "^9.0.2",
|
||||
"jsdom": "^24.1.0",
|
||||
"locize-cli": "^8.0.1",
|
||||
"postcss": "^8.4.39",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import type { AvailableLanguages } from "@/stores/user";
|
||||
import i18next from "i18next";
|
||||
import Backend from "i18next-locize-backend";
|
||||
import { locizePlugin } from "locize";
|
||||
|
||||
import { nextTick } from "vue";
|
||||
|
||||
|
|
@ -21,23 +19,42 @@ export function i18nextInit() {
|
|||
// .use(LanguageDetector)
|
||||
// init i18next
|
||||
// for all options read: https://www.i18next.com/overview/configuration-options
|
||||
.use(Backend)
|
||||
.use(locizePlugin)
|
||||
// .use(Backend)
|
||||
// .use(locizePlugin)
|
||||
.init({
|
||||
debug: true,
|
||||
supportedLngs: SUPPORT_LOCALES,
|
||||
fallbackLng: "de",
|
||||
defaultNS: "translation",
|
||||
returnNull: false,
|
||||
saveMissing: import.meta.env.DEV,
|
||||
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"],
|
||||
saveMissing: false,
|
||||
keySeparator: false,
|
||||
resources: {
|
||||
de: {
|
||||
translation: {
|
||||
"general.title": "myVBV",
|
||||
},
|
||||
},
|
||||
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) {
|
||||
// load locale messages with dynamic import
|
||||
// unused with locize
|
||||
const messages = await import(`./locales/${locale}.json`);
|
||||
let messages = null;
|
||||
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();
|
||||
}
|
||||
|
|
|
|||
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 * as Sentry from "@sentry/vue";
|
||||
import i18next from "i18next";
|
||||
|
|
@ -48,6 +48,8 @@ Sentry.init({
|
|||
});
|
||||
|
||||
i18nextInit().then(() => {
|
||||
loadI18nextLocaleMessages("de").then(() => {
|
||||
app.use(I18NextVue, { i18next });
|
||||
app.mount("#app");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ function findUserPointsHtml(userId: string) {
|
|||
|
||||
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(
|
||||
"a.Nicht bestanden"
|
||||
"a.Nicht Bestanden"
|
||||
)}</span>`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { bustItGetCache, itGetCached, itPost } from "@/fetchHelpers";
|
||||
import { setI18nLanguage } from "@/i18nextWrapper";
|
||||
import { loadI18nextLocaleMessages, setI18nLanguage } from "@/i18nextWrapper";
|
||||
import type { Country, CourseProfile } from "@/services/entities";
|
||||
import { directUpload } from "@/services/files";
|
||||
import dayjs from "dayjs";
|
||||
|
|
@ -106,6 +106,7 @@ async function setLocale(language: AvailableLanguages) {
|
|||
}
|
||||
dayjs.locale(language);
|
||||
setI18nLanguage(language);
|
||||
loadI18nextLocaleMessages(language);
|
||||
}
|
||||
|
||||
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