Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .github/workflows/i18n-sync.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# SPDX-FileCopyrightText: 2025 The BAR Lobby Authors
#
# SPDX-License-Identifier: CC0-1.0

name: I18n Sync

on:
push:
branches:
- master
paths:
- "lang/en/**"
schedule:
- cron: "0 6 * * *"
workflow_dispatch:

jobs:
push-source:
name: Push source strings to Transifex
if: github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v6

- name: Push source files to Transifex
uses: transifex/cli-action@v2
with:
token: ${{ secrets.TX_TOKEN }}
args: push --source

pull-translations:
name: Pull translations from Transifex
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v6

- name: Install Node.js
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
cache: npm

- name: Install dependencies
run: npm ci

- name: Pull translations from Transifex
uses: transifex/cli-action@v2
with:
token: ${{ secrets.TX_TOKEN }}
args: pull --all --force

- name: Regenerate i18n asset files
run: npm run generate-i18n-assets

- name: Create pull request
uses: peter-evans/create-pull-request@v7
with:
commit-message: "Update translations from Transifex"
title: "Update translations from Transifex"
body: |
Automated pull of latest translations from Transifex.

This PR updates `lang/` source files and regenerates `src/renderer/assets/languages/`.
branch: i18n/update-translations
delete-branch: true
6 changes: 6 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Validate i18n assets
run: |
npm run generate-i18n-assets
git diff --exit-code src/renderer/assets/languages/
npm run i18n:validate

- name: Format check
run: npm run format:check

Expand Down
31 changes: 31 additions & 0 deletions .tx/config
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[main]
host = https://app.transifex.com

# NOTE: Replace <org> and <project> with the actual Transifex organization
# and project slugs before activating the sync workflow.
# Resource type is KEYVALUEJSON for nested JSON translation files.

[o:<org>:p:<project>:r:lobby]
source_file = lang/en/lobby.json
file_filter = lang/<lang>/lobby.json
type = KEYVALUEJSON

[o:<org>:p:<project>:r:interface]
source_file = lang/en/interface.json
file_filter = lang/<lang>/interface.json
type = KEYVALUEJSON

[o:<org>:p:<project>:r:features]
source_file = lang/en/features.json
file_filter = lang/<lang>/features.json
type = KEYVALUEJSON

[o:<org>:p:<project>:r:tips]
source_file = lang/en/tips.json
file_filter = lang/<lang>/tips.json
type = KEYVALUEJSON

[o:<org>:p:<project>:r:units]
source_file = lang/en/units.json
file_filter = lang/<lang>/units.json
type = KEYVALUEJSON
1 change: 1 addition & 0 deletions REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ version = 1
# We don't consider the top level configuration files copyrightable in any way
path = [
".*",
".tx/config",
"package*.json",
"tsconfig.*",
"eslint.config.mjs",
Expand Down
116 changes: 114 additions & 2 deletions lang/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,118 @@ SPDX-FileCopyrightText: 2025 The BAR Lobby Authors
SPDX-License-Identifier: MIT
-->

# What is this directory?
# Translation Source Files

This directory and it's contents are a placeholder for the future git submodule that will manage translation files for both this repository and the main game repository, that will it self be managed through transifax.
This directory contains the source translation files for bar-lobby. Each locale
has its own subdirectory (e.g. `en/`, `de/`, `fr/`) with one or more JSON files
organized by domain.

## File Structure

```
lang/
├── en/ ← reference locale (source of truth)
│ ├── lobby.json
│ ├── interface.json
│ ├── features.json
│ ├── tips.json
│ └── units.json
├── de/
│ ├── lobby.json
│ └── ...
└── ...
```

**Do not edit** `src/renderer/assets/languages/*.json` by hand — those files are
generated by merging the per-domain files here into a single file per locale.

## How It Works

1. **Source files live here** (`lang/{locale}/*.json`). English (`en`) is the
reference locale and must be complete (no `null` values).
2. **Translators work in Transifex.** Each JSON file maps to a separate Transifex
resource so translators can track progress per domain.
3. **Automated sync** via GitHub Actions (`.github/workflows/i18n-sync.yml`):
- When `lang/en/` changes on `master`, source strings are pushed to Transifex.
- On a daily schedule (or manual trigger), translations are pulled from
Transifex, assets are regenerated, and a PR is opened automatically.
4. **Asset generation** merges per-domain files into the single-file-per-locale
format that `vue-i18n` loads at runtime.

## Developer Workflow

### Adding a new translation string

1. Add the key and English text to the appropriate file in `lang/en/` (e.g.
`lobby.json` for lobby UI strings).
2. Run `npm run generate-i18n-assets` to regenerate the runtime assets.
3. Reference the key in your component:
```vue
<script setup>
import { useTypedI18n } from "@renderer/i18n";
const { t } = useTypedI18n();
</script>
<template>
<Button>{{ t("lobby.buttons.quickPlay") }}</Button>
</template>
```
4. TypeScript autocomplete will suggest valid keys. See the
[wiki guide](https://github.com/beyond-all-reason/bar-lobby/wiki/Internationalization:-translation-strings-in-bar%E2%80%90lobby)
for full details.

### Finding unused or missing keys

Run the report script to see which keys exist in the language files but are not
referenced in code (unused), or which keys are referenced in code but missing
from the language files:

```sh
npm run i18n:report
```

### Validating translations

Run the validation script to check that the English reference locale is
complete and see a coverage summary for all locales:

```sh
npm run i18n:validate
```

CI runs this automatically — the build will fail if `en` has any `null` values
or if generated assets are out of date with source files.

### Adding a new locale

1. Create a new directory under `lang/` (e.g. `lang/pl/`).
2. Add JSON files matching the English structure. Missing keys will be set to
`null` by the generate script.
3. Run `npm run generate-i18n-assets`.
4. Register the new locale in `src/renderer/i18n.ts` (import and add to
`messages`).
5. Add the locale's Transifex resource mappings to `.tx/config`.

## Transifex Setup

The automated workflow requires:

- A **Transifex project** with resources matching those defined in `.tx/config`.
- A **`TX_TOKEN` repository secret** containing a Transifex API token.

Replace the `<org>` and `<project>` placeholders in `.tx/config` with actual
Transifex slugs before activating the workflow.

### Manual Transifex CLI usage

Install the [Transifex CLI](https://github.com/transifex/cli), then:

```sh
# Push English source strings to Transifex
TX_TOKEN=your_token tx push --source

# Pull all translations from Transifex
TX_TOKEN=your_token tx pull --all --force

# Regenerate assets after pulling
npm run generate-i18n-assets
```
74 changes: 74 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@
"typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false",
"typecheck": "concurrently \"npm run typecheck:node\" \"npm run typecheck:web\"",
"checks": "concurrently \"npm run typecheck:web\" \"npm run typecheck:node\" \"npm run lint\" \"npm run format:check\"",
"generate-i18n-assets": "node --import=tsx tools/generate-i18n-asset-files.ts"
"generate-i18n-assets": "node --import=tsx tools/generate-i18n-asset-files.ts",
"i18n:validate": "node --import=tsx tools/validate-i18n.ts",
"i18n:report": "vue-i18n-extract report --vueFiles \"src/renderer/**/*.{vue,ts}\" --languageFiles \"src/renderer/assets/languages/en.json\""
},
"engines": {
"node": "22.18.0"
Expand Down Expand Up @@ -104,6 +106,7 @@
"vite-plugin-static-copy": "^3.1.2",
"vite-plugin-vue-devtools": "^8.0.0",
"vitest": "^3.2.4",
"vue-i18n-extract": "^2.0.7",
"vue-tsc": "^3.0.3",
"wait-on": "^8.0.4"
}
Expand Down
6 changes: 3 additions & 3 deletions tools/generate-i18n-asset-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ async function getLanguageFiles(locale: string) {
return new Promise<Array<string>>((resolve, reject) => {
fs.glob(path.join(LANG_DIR, locale, "**/*.json"), (err, matches) => {
if (err) reject(err);
else resolve(matches);
else resolve(matches.sort());
});
});
}
Expand Down Expand Up @@ -104,7 +104,7 @@ async function main() {
const referenceObject = await processLocale(REFERENCE_LOCALE);

const referenceOutputPath = path.join(OUTPUT_DIR, `${REFERENCE_LOCALE}.json`);
fs.writeFileSync(referenceOutputPath, JSON.stringify(referenceObject, null, 2));
fs.writeFileSync(referenceOutputPath, JSON.stringify(referenceObject, null, 4) + "\n");
logAssetGeneration(referenceOutputPath);

for (const locale of locales) {
Expand All @@ -116,7 +116,7 @@ async function main() {
const completeLocaleObject = addMissingKeys(referenceObject, localeObject);

const outputFilePath = path.join(OUTPUT_DIR, `${locale}.json`);
fs.writeFileSync(outputFilePath, JSON.stringify(completeLocaleObject, null, 2));
fs.writeFileSync(outputFilePath, JSON.stringify(completeLocaleObject, null, 4) + "\n");
logAssetGeneration(outputFilePath);
}
}
Expand Down
Loading
Loading