VBV-746: Neuer technischer Prozess Übersetzungen

This commit is contained in:
Daniel Egger 2024-09-23 09:39:07 +02:00
parent 31fc9f46aa
commit 4274d47207
15 changed files with 4427 additions and 1112 deletions

View File

@ -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.
![](./docs/assets/babeledit01.png)
Here are the settings that I use in BabelEdit:
![](./docs/assets/babeledit02.png)
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

1
client/.gitignore vendored
View File

@ -26,3 +26,4 @@ coverage
*.njsproj
*.sln
*.sw?
babel-edit.babel

View File

@ -1,6 +1,7 @@
{
"htmlWhitespaceSensitivity": "ignore",
"jsonRecursiveSort": true,
"jsonSortOrder": "{\"/.*/\": \"caseInsensitiveNumeric\"}",
"organizeImportsSkipDestructiveCodeActions": true,
"plugins": [
"prettier-plugin-sort-json",

View File

@ -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,
// }
};

2305
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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

View File

@ -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(() => {
app.use(I18NextVue, { i18next });
app.mount("#app");
loadI18nextLocaleMessages("de").then(() => {
app.use(I18NextVue, { i18next });
app.mount("#app");
});
});

View File

@ -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>`;
}
}

View File

@ -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({

BIN
docs/assets/babeledit01.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 KiB

BIN
docs/assets/babeledit02.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB