Skip to content

Conversation

@Aotumuri
Copy link
Member

@Aotumuri Aotumuri commented Dec 31, 2025

Resolves #2739

Description:

Introduce language metadata handling and refactor existing language checks to validate the existence of language JSON and corresponding SVG files. Add tests to ensure the integrity of the new metadata structure and its references.

The lang field is intentionally kept in each language file.
This is because the files are frequently regenerated by Crowdin, and the field also serves as a hint for management and maintenance.

Please complete the following:

  • I have added screenshots for all UI updates
  • I process any text displayed to the user through translateText() and I've added it to the en.json file
  • I have added relevant tests to the test directory
  • I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced

Please put your Discord username so you can be contacted if a bug or regression is found:

aotumuri

@Aotumuri Aotumuri requested review from a team as code owners December 31, 2025 01:28
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 31, 2025

Walkthrough

This PR introduces a metadata-driven architecture for language handling, moving from static embedded language data to dynamic async loading. A new metadata.json file centralizes language information, LangSelector.ts is refactored to load languages asynchronously with caching, and test coverage is updated to validate metadata consistency with resource files.

Changes

Cohort / File(s) Summary
Language Metadata Foundation
resources/lang/metadata.json
New data file defining language entries with code, native/English names, and SVG icon identifiers. Covers multiple languages with varied code formats (two-letter, hyphenated, region-specific). Provides centralized source for language information.
LangSelector Async Refactoring
src/client/LangSelector.ts
Replaced static language map with metadata-driven approach. Introduced LanguageMetadata type and async methods: loadLanguage(), loadLanguageList(), changeLanguage(), openModal(), and initializeLanguage(). Added language cache and per-language JSON fetching. Updated getClosestSupportedLang() logic with debug code path support and base-language fallbacks. Exposed public state for translations, defaultTranslations, and currentLang.
Test Suite Migration
tests/LangCode.test.ts (removed)
tests/LangSvg.test.ts (removed)
tests/LangMetadata.test.ts (added)
Removed old tests validating filename/lang_code consistency and per-file SVG references. Added new comprehensive test validating that metadata.json entries correspond to existing language JSON files and flag SVG assets, with descriptive error collection.

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant LS as LangSelector
    participant MetaFile as metadata.json
    participant LangFiles as Language JSONs
    participant FlagFiles as Flag SVGs

    rect rgb(240, 248, 255)
    Note over App,FlagFiles: Initialization Phase
    App->>LS: initializeLanguage()
    LS->>MetaFile: Load metadata.json
    MetaFile-->>LS: Language metadata entries
    par
        LS->>LangFiles: Fetch user language JSON
        LS->>LangFiles: Fetch default (en) JSON
    and
        Note over LS: Compute supported codes<br/>from metadata
    end
    LangFiles-->>LS: Translation data
    LS->>LS: Cache translations
    LS->>LS: Apply translations to UI
    end

    rect rgb(245, 245, 220)
    Note over App,FlagFiles: Language Change Flow
    App->>LS: changeLanguage(lang)
    LS->>LS: getClosestSupportedLang()
    LS->>LangFiles: loadLanguage(closestLang)
    LangFiles-->>LS: Translation data
    LS->>LS: Cache & apply translations
    LS->>LS: Update languageList from metadata
    LS->>LS: Re-render with new language
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

Feature - Frontend, Translation, Feature - Test

Suggested reviewers

  • evanpelle
  • scottanderson
  • drillskibo

Poem

📦 No more bundles fat with all the tongues,
Fetch each language light and on-the-fly,
Metadata calls the shots, async funds,
Swift cache-backed loading—translations fly! 🚀

Pre-merge checks

✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add language metadata and enhance language validation tests' clearly and accurately describes the main changes: introducing metadata handling and refactoring language validation tests.
Description check ✅ Passed The description relates to the changeset by explaining the metadata handling introduction and test refactoring, matching the changes in the pull request files.
Linked Issues check ✅ Passed The PR successfully addresses issue #2739 by introducing a language metadata file to avoid bundling individual language files and enabling runtime fetching, with LangSelector refactored to use metadata-driven approach.
Out of Scope Changes check ✅ Passed All changes are directly aligned with the objectives: metadata.json addition, LangSelector refactoring for metadata-driven loading, and test updates to validate the new structure. No out-of-scope changes detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6dd3ce8 and 0c275a3.

📒 Files selected for processing (1)
  • src/client/LangSelector.ts
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: TheGiraffe3
Repo: openfrontio/OpenFrontIO PR: 884
File: resources/lang/en.json:456-461
Timestamp: 2025-08-16T10:52:08.292Z
Learning: In OpenFrontIO, translation files in resources/lang/*.json (except en.json) should not be updated in regular PRs. Only dedicated translation PRs titled "mls" and made by Aotumori should update non-English locale files. Regular PRs should only update en.json when adding or modifying translation keys.
Learnt from: andrewNiziolek
Repo: openfrontio/OpenFrontIO PR: 1007
File: resources/lang/de.json:115-115
Timestamp: 2025-06-02T14:27:37.609Z
Learning: For OpenFrontIO project: When localization keys are renamed in language JSON files, the maintainers separate technical changes from translation content updates. They wait for community translators to update the actual translation values rather than attempting to translate in the same PR. This allows technical changes to proceed while ensuring accurate translations from native speakers.
Learnt from: Aotumuri
Repo: openfrontio/OpenFrontIO PR: 1534
File: src/client/LangSelector.ts:97-106
Timestamp: 2025-07-23T12:36:35.354Z
Learning: In OpenFrontIO's LangSelector.ts, the getClosestSupportedLang method always joins language code parts with underscores ("_") because all keys in the languageMap use underscore format (e.g., pt_BR, sv_SE, zh_CN). This normalization ensures consistency regardless of whether the input language code uses hyphens or underscores as delimiters.
📚 Learning: 2025-07-23T12:36:35.354Z
Learnt from: Aotumuri
Repo: openfrontio/OpenFrontIO PR: 1534
File: src/client/LangSelector.ts:97-106
Timestamp: 2025-07-23T12:36:35.354Z
Learning: In OpenFrontIO's LangSelector.ts, the getClosestSupportedLang method always joins language code parts with underscores ("_") because all keys in the languageMap use underscore format (e.g., pt_BR, sv_SE, zh_CN). This normalization ensures consistency regardless of whether the input language code uses hyphens or underscores as delimiters.

Applied to files:

  • src/client/LangSelector.ts
📚 Learning: 2025-06-02T14:27:37.609Z
Learnt from: andrewNiziolek
Repo: openfrontio/OpenFrontIO PR: 1007
File: resources/lang/de.json:115-115
Timestamp: 2025-06-02T14:27:37.609Z
Learning: For OpenFrontIO project: When localization keys are renamed in language JSON files, the maintainers separate technical changes from translation content updates. They wait for community translators to update the actual translation values rather than attempting to translate in the same PR. This allows technical changes to proceed while ensuring accurate translations from native speakers.

Applied to files:

  • src/client/LangSelector.ts
🔇 Additional comments (9)
src/client/LangSelector.ts (9)

8-13: LGTM! Clean type definition.

The flat LanguageMetadata type is clear and uses simple field names. Good use of a typed structure over a class hierarchy.


6-6: Good: metadata and cache setup align with PR objectives.

Bundling metadata.json (which is small) while loading individual language files at runtime achieves the goal of reducing bundle size from ~1MB. The cache map is properly typed.

Also applies to: 25-26


47-63: LGTM! Language matching logic is sound.

The metadata-driven approach correctly handles exact matches, base language fallbacks, and debug mode. The Set construction on each call is fine given the small metadata size.


65-81: LGTM! Efficient parallel loading.

Using Promise.all to load English and user language translations in parallel is efficient. The initialization sequence (translations → language list → apply) is correct.


83-107: LGTM! Robust language loading with proper caching.

The implementation correctly:

  • Caches loaded languages to avoid repeated fetches
  • Uses encodeURIComponent to prevent path traversal
  • Handles errors gracefully with fallback to empty object
  • Special-cases English to use bundled data

113-113: Existing error handling is adequate.

The past review comment about Intl.Locale potentially throwing is addressed by the outer try-catch block (lines 110-166). If the constructor throws, the error is caught, logged, and the language list remains empty, allowing graceful degradation.


126-134: LGTM! Metadata-driven list building is correct.

The iteration over languageMetadata correctly builds the language list and properly handles the debug language exclusion logic.


169-175: LGTM! Async language change is correctly implemented.

The method properly awaits language loading before updating state and applying translations. The sequence prevents race conditions.


244-248: LGTM! Modal opening with async list loading.

Awaiting loadLanguageList before showing the modal ensures the language list is ready when the modal displays. The debug mode handling is appropriate.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (7)
tests/LangMetadata.test.ts (2)

10-23: Consider using explicit test skip instead of silent return.

Using console.log and return makes the test appear as "passed" when metadata is missing or empty. If the file should always exist, consider failing the test. If skipping is intentional, use Jest's skip mechanism for clearer test reports.

🔎 Option: Fail explicitly if metadata is required
    if (!fs.existsSync(metadataFile)) {
-     console.log(
-       "No resources/lang/metadata.json file found. Skipping check.",
-     );
-     return;
+     throw new Error("resources/lang/metadata.json is required but not found");
    }

    const metadata = JSON.parse(fs.readFileSync(metadataFile, "utf-8"));
    if (!Array.isArray(metadata) || metadata.length === 0) {
-     console.log(
-       "No language entries found in metadata.json. Skipping check.",
-     );
-     return;
+     throw new Error("metadata.json must contain a non-empty array of languages");
    }

17-17: Wrap JSON.parse in try-catch for clearer error on malformed JSON.

If metadata.json contains invalid JSON, this will throw a generic parse error. A try-catch would let you provide a clearer failure message.

🔎 Suggested improvement
-   const metadata = JSON.parse(fs.readFileSync(metadataFile, "utf-8"));
+   let metadata: unknown;
+   try {
+     metadata = JSON.parse(fs.readFileSync(metadataFile, "utf-8"));
+   } catch (err) {
+     throw new Error(`Failed to parse metadata.json: ${err}`);
+   }
src/client/LangSelector.ts (5)

44-65: Potential race condition on concurrent calls.

If getLanguageMetadata() is called twice before the first fetch completes, both calls will trigger a network request. Consider storing the pending promise to deduplicate concurrent requests.

🔎 Suggested fix using promise caching
  private languageMetadata: LanguageMetadata[] | null = null;
+ private languageMetadataPromise: Promise<LanguageMetadata[]> | null = null;
  private languageCache = new Map<string, Record<string, string>>();

  private async getLanguageMetadata(): Promise<LanguageMetadata[]> {
    if (this.languageMetadata) return this.languageMetadata;
+   if (this.languageMetadataPromise) return this.languageMetadataPromise;

+   this.languageMetadataPromise = this.fetchLanguageMetadata();
+   return this.languageMetadataPromise;
+ }
+
+ private async fetchLanguageMetadata(): Promise<LanguageMetadata[]> {
    try {
      const response = await fetch("/lang/metadata.json");
      // ... rest of implementation
    } catch (err) {
      console.error("Failed to load language metadata:", err);
      this.languageMetadata = [];
    }

    return this.languageMetadata;
  }

54-57: Type assertion trusts server response shape.

The cast as LanguageMetadata[] assumes the response matches the expected shape. Since metadata.json is a controlled static file, this is likely fine. For extra safety, you could validate that each entry has the required fields.


105-123: Same race condition pattern as metadata loading.

Similar to getLanguageMetadata, concurrent calls for the same language before the first completes will trigger duplicate fetches. Consider caching the pending promise in a Map.

🔎 Suggested fix
  private languageCache = new Map<string, Record<string, string>>();
+ private languageLoadPromises = new Map<string, Promise<Record<string, string>>>();

  private async loadLanguage(lang: string): Promise<Record<string, string>> {
    if (!lang) return {};
    const cached = this.languageCache.get(lang);
    if (cached) return cached;
+   const pending = this.languageLoadPromises.get(lang);
+   if (pending) return pending;

+   const promise = this.fetchLanguage(lang);
+   this.languageLoadPromises.set(lang, promise);
+   return promise;
+ }
+
+ private async fetchLanguage(lang: string): Promise<Record<string, string>> {
    try {
      const response = await fetch(`/lang/${encodeURIComponent(lang)}.json`);
      // ... rest
    } catch (err) {
      console.error(`Failed to load language ${lang}:`, err);
      return {};
+   } finally {
+     this.languageLoadPromises.delete(lang);
    }
  }

17-17: Consider using typed union instead of any[].

languageList: any[] loses type safety. You already have LanguageMetadata type - consider using it or a similar type for the list.

🔎 Suggested fix
- @state() private languageList: any[] = [];
+ @state() private languageList: LanguageMetadata[] = [];

Then update the debug entry to match the type, and remove other any annotations in loadLanguageList.


261-265: Modal opens before language list finishes loading.

Setting showModal = true before awaiting loadLanguageList() could briefly show an empty list. If the list is cached, this is likely fast enough. Consider loading before showing for smoother UX, or accept current behavior.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b5a0541 and 834d72e.

📒 Files selected for processing (5)
  • resources/lang/metadata.json
  • src/client/LangSelector.ts
  • tests/LangCode.test.ts
  • tests/LangMetadata.test.ts
  • tests/LangSvg.test.ts
💤 Files with no reviewable changes (2)
  • tests/LangCode.test.ts
  • tests/LangSvg.test.ts
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: TheGiraffe3
Repo: openfrontio/OpenFrontIO PR: 884
File: resources/lang/en.json:456-461
Timestamp: 2025-08-16T10:52:08.292Z
Learning: In OpenFrontIO, translation files in resources/lang/*.json (except en.json) should not be updated in regular PRs. Only dedicated translation PRs titled "mls" and made by Aotumori should update non-English locale files. Regular PRs should only update en.json when adding or modifying translation keys.
Learnt from: andrewNiziolek
Repo: openfrontio/OpenFrontIO PR: 1007
File: resources/lang/de.json:115-115
Timestamp: 2025-06-02T14:27:37.609Z
Learning: For OpenFrontIO project: When localization keys are renamed in language JSON files, the maintainers separate technical changes from translation content updates. They wait for community translators to update the actual translation values rather than attempting to translate in the same PR. This allows technical changes to proceed while ensuring accurate translations from native speakers.
📚 Learning: 2025-08-16T10:52:08.292Z
Learnt from: TheGiraffe3
Repo: openfrontio/OpenFrontIO PR: 884
File: resources/lang/en.json:456-461
Timestamp: 2025-08-16T10:52:08.292Z
Learning: In OpenFrontIO, translation files in resources/lang/*.json (except en.json) should not be updated in regular PRs. Only dedicated translation PRs titled "mls" and made by Aotumori should update non-English locale files. Regular PRs should only update en.json when adding or modifying translation keys.

Applied to files:

  • resources/lang/metadata.json
📚 Learning: 2025-08-27T08:12:19.610Z
Learnt from: mokizzz
Repo: openfrontio/OpenFrontIO PR: 1940
File: resources/lang/en.json:763-766
Timestamp: 2025-08-27T08:12:19.610Z
Learning: In OpenFrontIO, some country entries in src/client/data/countries.json may have similar names but different codes (e.g., "Empire of Japan" vs "Empire of Japan1"). Each unique code requires its own translation key in resources/lang/en.json after normalization. Always verify against countries.json before suggesting removal of translation keys.

Applied to files:

  • resources/lang/metadata.json
📚 Learning: 2025-07-23T12:36:35.354Z
Learnt from: Aotumuri
Repo: openfrontio/OpenFrontIO PR: 1534
File: src/client/LangSelector.ts:97-106
Timestamp: 2025-07-23T12:36:35.354Z
Learning: In OpenFrontIO's LangSelector.ts, the getClosestSupportedLang method always joins language code parts with underscores ("_") because all keys in the languageMap use underscore format (e.g., pt_BR, sv_SE, zh_CN). This normalization ensures consistency regardless of whether the input language code uses hyphens or underscores as delimiters.

Applied to files:

  • src/client/LangSelector.ts
📚 Learning: 2025-06-02T14:27:37.609Z
Learnt from: andrewNiziolek
Repo: openfrontio/OpenFrontIO PR: 1007
File: resources/lang/de.json:115-115
Timestamp: 2025-06-02T14:27:37.609Z
Learning: For OpenFrontIO project: When localization keys are renamed in language JSON files, the maintainers separate technical changes from translation content updates. They wait for community translators to update the actual translation values rather than attempting to translate in the same PR. This allows technical changes to proceed while ensuring accurate translations from native speakers.

Applied to files:

  • src/client/LangSelector.ts
🧬 Code graph analysis (1)
tests/LangMetadata.test.ts (1)
eslint.config.js (1)
  • __dirname (10-10)
🔇 Additional comments (7)
resources/lang/metadata.json (1)

1-206: Structure looks good.

The metadata file has a clean, consistent schema across all 35 language entries. Each entry correctly includes code, native, en, and svg fields. The test file will validate these against actual resource files.

tests/LangMetadata.test.ts (1)

25-60: Good error collection pattern.

Collecting all validation errors before failing with expect(errors).toEqual([]) is a clean approach. This shows all missing files at once rather than failing on the first issue.

src/client/LangSelector.ts (5)

5-10: Clean type definition.

The LanguageMetadata type matches the metadata.json structure well. Simple typed object, no class hierarchy - good choice.


67-83: Good language matching logic.

The fallback to base code matching with preference for more specific codes (sorting by length) is a sensible approach. Returns "en" as safe default.


85-103: Good parallel loading pattern.

Using Promise.all to load default and user translations concurrently is efficient. The flow is clear: resolve language → load translations → apply.


186-192: Clean language switching implementation.

Saves preference, loads translations, updates state, applies changes. Simple and correct.


312-331: Good recursive flattening implementation.

Clean, pure function that handles nested translation objects. Warning on unexpected types helps catch data issues.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
coderabbitai[bot]
coderabbitai bot previously approved these changes Dec 31, 2025
if (this.languageMetadata) return this.languageMetadata;

try {
const response = await fetch("/lang/metadata.json");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think /lang/metadata.json and en.json should be built into the bundle itself since we always need to fetch it anways.

@evanpelle evanpelle added this to the v29 milestone Dec 31, 2025
@Aotumuri Aotumuri requested a review from evanpelle January 2, 2026 02:27
Copy link
Collaborator

@evanpelle evanpelle left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@evanpelle evanpelle merged commit ae6293f into openfrontio:main Jan 3, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Don't include lang files in bundle

4 participants