Files
2024-10-11 16:28:05 +11:00

221 lines
6.6 KiB
Markdown

# Internationalization
!!! warning
**Important notice:** please don't translate server logs with `i18n.*()`. It uses the request locale, not the server one.
## Quick links to Crowdin
- [Project Dashboard](https://crowdin.com/project/pixivfe)
- [Project Settings](https://crowdin.com/project/pixivfe/settings)
- [Translation Editor](https://crowdin.com/editor/pixivfe)
- [API Key](https://crowdin.com/settings#api-key)
## Todo
- [x] Add cookie: Locale
- [x] Rewrite jet template on load to use translated strings
- [ ] Add dedicated option to set locale in settings page
- [ ] Check if any jet template strings are ignored (false negative)
- [x] Setup Crowdin
- [x] manual upload and download
- [x] automatic upload and download
- [ ] automated upload and download via Woodpecker CI
## Crowdin CLI usage
Install the CLI: [See official instructions](https://crowdin.github.io/crowdin-cli/installation)
Remember to check `crowdin -V`. Should be `4.2.0` or later.
First, set up API token:
1. Go to [https://crowdin.com/settings#api-key](https://crowdin.com/settings#api-key)
2. Click the "New Token" button
3. Set permission: Projects >
- Projects (List, Get, Create, Edit) -- Read only
- Source files & strings (List, Get, Create, Edit) -- Read and write
- Translations (List, Get, Create, Edit) -- Read and write
4. Copy the new token and save it somewhere
Then, try it out.
```sh
export CROWDIN_PERSONAL_TOKEN=token_here # put this somewhere in your shell config, or the `.env` file inside this repo, which will be used by ./build.sh
crowdin upload
crowdin download
```
## Crowdin Docker usage
These instructions assume `CROWDIN_PERSONAL_TOKEN` is available as an environment variable.
### Step 0: Generate i18n source files
Create `locale/en` source files (`code.json` and `template.json`):
```sh
./build.sh i18n
```
### Step 1: Set up Docker container
Create an interactive container using `crowdin/cli:latest`, mounting local files:
```sh
docker run -it --rm --name crowdin-cli \
-e CROWDIN_PERSONAL_TOKEN=${CROWDIN_PERSONAL_TOKEN} \
-v "./i18n/locale:/usr/crowdin-project/i18n/locale" \
-v "./crowdin.yml:/usr/crowdin-project/crowdin.yml" \
crowdin/cli:latest
```
### Step 2: Upload local sources to Crowdin
Update `locale/en` sources on the [Crowdin project](https://crowdin.com/project/pixivfe/sources/files):
```sh
crowdin upload --verbose
```
### Step 3: Download translations to local repository
Fetch new translations from [Crowdin](https://crowdin.com/project/pixivfe) and update non-English locales:
```sh
crowdin download --verbose
```
## Crowdin Web UI usage
Add new languages to translate in Settings > Languages.
## ./i18n/crawler -- Primer on html and jet's treatment of templates
```
<a>abc {{.Hi}}</a>
```
html: `abc {{.Hi}}` is text
jet: `<a>abc ` is text
jet: `</a>` is text
So, jet doesn't care about HTML.
And we have to care about `{* *}` comments.
## ./i18n/crawler -- Coalesce inline tags
Example: in below, the `<a>` tag should be part of the string.
```html
Log in with your Pixiv account's cookie to access features above. To learn how to obtain your cookie, please see <a href="https://pixivfe-docs.pages.dev/obtaining-pixivfe-token/">the guide on obtaining your PixivFE token</a>.
```
Tags to consider as inline: `a`
## Implementation details
!!! warning
This section was written using an LLM with codebase context, and may contain inaccuracies
Should be used as a rough primer on the i18n implementation only
This section covers implementation details for the i18n system.
The i18n system consists of the following components:
| Component | Description | Key files/functions |
|-----------|-------------|---------------------|
| Crawler | Extracts translatable strings from HTML template files | `crawler/main.go` |
| Converter | Processes crawler output to generate translation map | `converter/main.go`, `i18n.SuccintId()` |
| Locale files | Store `en` source and translations in JSON format | `i18n/locale/<lang_code>/code.json`, `i18n/locale/<lang_code>/template.json` |
| Lookup and rewrite functions | Core i18n functionality for loading translations and looking up strings | `lookup.go`, `rewrite.go` |
| Integration | Wrapper functions for automatic translation lookup | `Tr()`, `Sprintf()` |
Additional notes:
- Uses `xxHash` for string hashing when generating IDs
- Caches `strings.Replacer` objects for performance
- Supports different locales per goroutine using `routine.InheritableThreadLocal`
- Includes a Semgrep rule (`semgrep-i18n.yml`) for detecting untranslated strings
### 1. Crawler
The crawler (`crawler/main.go`) scans HTML template files to extract translatable strings.
- Uses the `html` package to parse HTML
- Traverses the DOM tree to find text nodes
- Ignores certain patterns (e.g., Jet template commands and strings included in the `IgnoreTheseStrings` variable)
- Outputs a JSON array of objects containing the message and file path
**Example output:**
```json
[
{
"msg": "Translatable string",
"file": "path/to/file.html"
}
]
```
### 2. Converter
The converter (`converter/main.go`) processes the crawler output to generate a translation map.
- Reads the crawler JSON from stdin
- Generates a unique ID for each string using `i18n.SuccintId()`
- Outputs a JSON object mapping IDs to original strings
**Example output:**
```json
{
"path/to/file.html:uniqueId": "Translatable string"
}
```
### 3. Locale files
Translations are stored in JSON files under `i18n/locale/<lang_code>/`:
- `code.json`: Translations for strings in Go code
- `template.json`: Translations for strings in HTML templates
The base locale (English) contains the original strings, while other locales contain translated strings.
### 4. Lookup and rewrite functions
The core i18n functionality is implemented in `lookup.go` and `rewrite.go`.
#### 4.1. Lookup
`lookup.go` provides functions to load translations and look up strings:
- `Init()`: Loads all locale files into memory
- `__lookup_skip_stack_2()`: Performs the actual string lookup
- `SuccintId()`: Generates a unique ID for a string based on file path and content
#### 4.2. Rewrite
`rewrite.go` handles template rewriting:
- `Replacer()`: Returns a `strings.Replacer` for a given locale and file
- `translationPairs_inner()`: Generates replacement pairs for a locale and file
### 5. Integration
The i18n system is integrated into the application code using wrapper functions that automatically look up translations based on the current locale:
```go
func Tr(text string) string {
return __lookup_skip_stack_2(GetLocale(), text)
}
func Sprintf(format string, a ...any) string {
format = __lookup_skip_stack_2(GetLocale(), format)
return fmt.Sprintf(format, a...)
}
```