Skip to content

Translations POC#1354

Open
Blipz wants to merge 6 commits intoAzgaar:translationfrom
Blipz:translations
Open

Translations POC#1354
Blipz wants to merge 6 commits intoAzgaar:translationfrom
Blipz:translations

Conversation

@Blipz
Copy link
Copy Markdown

@Blipz Blipz commented Mar 13, 2026

Description

Proof-of-concept for translating FMG using i18next.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Proof-of-concept integration of i18next-based translations into FMG, including runtime label replacement and some locale-specific grammar handling.

Changes:

  • Added i18next initialization + custom formatters (gender/articles/prepositions) and wired it into module startup
  • Updated state naming and name suffix generation to use i18next translations
  • Added EN/FR locale JSONs and expanded index.html with data-text attributes for extraction/translation

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/utils/languageUtils.ts Adds locale-specific adjective rules and introduces a shared gender map
src/utils/index.ts Re-exports gender from utils barrel
src/modules/states-generator.ts Uses i18next for localized state full names and sets FR gender metadata
src/modules/names-generator.ts Localizes the default ia suffix via i18next
src/modules/locales.ts Initializes i18next + registers custom formatters + updates DOM labels from data-*
src/modules/index.ts Ensures locales initialization runs before other modules
src/index.html Adds data-text keys across UI elements for translation
public/main.js Attempts to await i18n initialization before continuing startup
public/locales/fr/lang.json Adds French translations and grammar templates
public/locales/en/lang.json Adds English baseline translations (identity mapping)
package.json Adds i18next deps and an extraction script
i18next.config.ts Adds i18next-cli extraction config plus HTML key extraction plugin

Comment thread public/main.js Outdated
Comment thread src/utils/languageUtils.ts Outdated
Comment on lines 36 to +38
export const getAdjective = (nounToBeAdjective: string) => {
const adjectivizationRules = [
{
name: "guo",
probability: 1,
condition: / Guo$/,
action: (noun: string) => noun.slice(0, -4),
},
{
name: "orszag",
probability: 1,
condition: /orszag$/,
action: (noun: string) =>
noun.length < 9 ? `${noun}ian` : noun.slice(0, -6),
},
{
name: "stan",
probability: 1,
condition: /stan$/,
action: (noun: string) =>
noun.length < 9 ? `${noun}i` : trimVowels(noun.slice(0, -4)),
},
{
name: "land",
probability: 1,
condition: /land$/,
action: (noun: string) => {
if (noun.length > 9) return noun.slice(0, -4);
const root = trimVowels(noun.slice(0, -4), 0);
if (root.length < 3) return `${noun}ic`;
if (root.length < 4) return `${root}lish`;
return `${root}ish`;
},
},
{
name: "que",
probability: 1,
condition: /que$/,
action: (noun: string) => noun.replace(/que$/, "can"),
},
{
name: "a",
probability: 1,
condition: /a$/,
action: (noun: string) => `${noun}n`,
},
{
name: "o",
probability: 1,
condition: /o$/,
action: (noun: string) => noun.replace(/o$/, "an"),
},
{
name: "u",
probability: 1,
condition: /u$/,
action: (noun: string) => `${noun}an`,
},
{
name: "i",
probability: 1,
condition: /i$/,
action: (noun: string) => `${noun}an`,
},
{
name: "e",
probability: 1,
condition: /e$/,
action: (noun: string) => `${noun}an`,
},
{
name: "ay",
probability: 1,
condition: /ay$/,
action: (noun: string) => `${noun}an`,
},
{
name: "os",
probability: 1,
condition: /os$/,
action: (noun: string) => {
const root = trimVowels(noun.slice(0, -2), 0);
if (root.length < 4) return noun.slice(0, -1);
return `${root}ian`;
},
},
{
name: "es",
probability: 1,
condition: /es$/,
action: (noun: string) => {
const root = trimVowels(noun.slice(0, -2), 0);
if (root.length > 7) return noun.slice(0, -1);
return `${root}ian`;
},
},
{
name: "l",
probability: 0.8,
condition: /l$/,
action: (noun: string) => `${noun}ese`,
},
{
name: "n",
probability: 0.8,
condition: /n$/,
action: (noun: string) => `${noun}ese`,
},
{
name: "ad",
probability: 0.8,
condition: /ad$/,
action: (noun: string) => `${noun}ian`,
},
{
name: "an",
probability: 0.8,
condition: /an$/,
action: (noun: string) => `${noun}ian`,
},
{
name: "ish",
probability: 0.25,
condition: /^[a-zA-Z]{6}$/,
action: (noun: string) => `${trimVowels(noun.slice(0, -1))}ish`,
},
{
name: "an",
probability: 0.5,
condition: /^[a-zA-Z]{0,7}$/,
action: (noun: string) => `${trimVowels(noun)}an`,
},
];
for (const rule of adjectivizationRules) {
const adjectivizationRules = {
en: [
Comment thread src/modules/locales.ts Outdated
Comment thread src/modules/locales.ts Outdated
Comment thread src/modules/states-generator.ts
Comment thread src/index.html Outdated
Comment thread src/index.html Outdated
@netlify
Copy link
Copy Markdown

netlify bot commented Mar 24, 2026

Deploy Preview for afmg ready!

Name Link
🔨 Latest commit c5f1577
🔍 Latest deploy log https://app.netlify.com/projects/afmg/deploys/69cd328ba195cd000805b378
😎 Deploy Preview https://deploy-preview-1354--afmg.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@Blipz
Copy link
Copy Markdown
Author

Blipz commented Apr 1, 2026

Most of the work to make strings translatable in index.html has been done, as well as support for provinces and states localized naming. For now, the identifier for each string is the English version.
I'll stop there to avoid conflicts with the ongoing module migration. Hopefully, this will be of some use in the future.
Perhaps a Crowdin project could be created beforehand?

@Azgaar
Copy link
Copy Markdown
Owner

Azgaar commented Apr 1, 2026

@Blipz, it looks pretty good. Yes, we would probably need Crowdin, but it has a significant lines count limit for free accounts.

@Blipz
Copy link
Copy Markdown
Author

Blipz commented Apr 1, 2026

@Azgaar Do you know there is a form to request a license for open source projects?
https://crowdin.com/product/for-open-source

@Azgaar
Copy link
Copy Markdown
Owner

Azgaar commented Apr 1, 2026

Yes, I saw it. Maybe we can apply. Would need to merge it to a separate branch.

@Azgaar Azgaar marked this pull request as ready for review April 1, 2026 17:32
@Azgaar Azgaar requested a review from Copilot April 1, 2026 17:33
@Azgaar Azgaar changed the base branch from master to translation April 1, 2026 17:33
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds an i18next-based proof-of-concept localization system to Fantasy Map Generator, replacing the old Google Translate UI flow and introducing French translations plus localized naming for some generated entities.

Changes:

  • Introduces runtime locale initialization + DOM label/tooltip translation updates via i18next (HTTP backend).
  • Localizes generated names for states/provinces (and a suffix in name generation) using translation keys + simple French grammatical formatters (gender/the/of).
  • Adds French locale resource file and i18next extraction tooling/config.

Reviewed changes

Copilot reviewed 14 out of 18 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/utils/languageUtils.ts Adds gender map and language-specific adjectivization rules used by state naming.
src/utils/index.ts Re-exports gender from utils bundle.
src/types/global.ts Declares initLocale / changeLocale globals.
src/modules/states-generator.ts Uses i18next to build localized state full names; injects French gender metadata.
src/modules/provinces-generator.ts Uses i18next to build localized province full names.
src/modules/names-generator.ts Uses i18next for the default “ia” suffix (locale-dependent naming).
src/modules/locales.ts New module: initializes i18next, adds formatters, updates DOM labels/tooltips.
src/modules/index.ts Imports the new locales module during module bootstrap.
public/modules/ui/options.js Wires language option changes to changeLocale; removes Google Translate controls; sets default language.
public/main.js Initializes locale on DOMContentLoaded before continuing app startup.
public/locales/fr/lang.json Adds French translation resource bundle (POC).
public/icons.css Adjusts sort icon pseudo-element content (adds NBSP).
package.json Adds i18next dependencies and an extraction script.
package-lock.json Locks new i18next/i18next-cli dependency graph.
i18next.config.ts Adds i18next-cli extraction config + HTML key extraction plugin.

Comment thread src/utils/languageUtils.ts Outdated
Comment thread src/modules/locales.ts
Comment on lines +16 to +30
for (const label of document.querySelectorAll("[data-html]")) {
let vars: TOptions = {
interpolation: { escapeValue: false },
};
for (const attr of label.attributes) {
if (attr.name.startsWith("data-var-")) {
vars[attr.name.slice(9)] = attr.value;
}
}
const translation = i18next.t(
label.getAttribute("data-html") as string,
vars,
);
if (translation) label.innerHTML = translation;
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

updateLabels sets interpolation: { escapeValue: false } and then writes translations into innerHTML. If any data-var-* values (or translated strings) can include user-controlled content, this enables HTML/script injection. Prefer keeping escaping enabled for interpolated values and only using innerHTML for known-safe, static markup (or sanitize via a library like DOMPurify before assigning).

Copilot uses AI. Check for mistakes.
Comment thread src/modules/locales.ts Outdated
noun: i18next.t(`stateForm::${state.formName}`),
complement: state.name,
gender: stateGender,
interpolation: { escapeValue: false },
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

getFullName passes interpolation: { escapeValue: false } while interpolating state.name into the translated string. State names are user-editable in the UI, so disabling escaping here can allow HTML injection wherever fullName is rendered via innerHTML. Keep escaping enabled for user-controlled values (or sanitize) and only disable escaping for specific, trusted markup tokens.

Suggested change
interpolation: { escapeValue: false },

Copilot uses AI. Check for mistakes.
Comment on lines +61 to 75
gender.fr = {
...gender.fr,
Confederation: "feminine",
Diarchy: "feminine",
Horde: "feminine",
League: "feminine",
Marches: "feminine",
Oligarchy: "feminine",
Tetrarchy: "feminine",
Theocracy: "feminine",
"Trade Company": "feminine",
Union: "feminine",
};

class StatesModule {
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

This module mutates gender.fr at import time (gender.fr = {...}), creating a hidden side effect and coupling translation metadata to the states generator. To keep this predictable and easier to extend, consider defining the gender map in a dedicated locale/translation module (or in languageUtils) and importing it, rather than mutating a shared object from within a generator module.

Suggested change
gender.fr = {
...gender.fr,
Confederation: "feminine",
Diarchy: "feminine",
Horde: "feminine",
League: "feminine",
Marches: "feminine",
Oligarchy: "feminine",
Tetrarchy: "feminine",
Theocracy: "feminine",
"Trade Company": "feminine",
Union: "feminine",
};
class StatesModule {
class StatesModule {
constructor() {
this.extendFrenchGender();
}
/**
* Extend the French gender map with government types relevant for states.
* This is done explicitly during StatesModule construction to avoid
* mutating shared translation state at module import time.
*/
private extendFrenchGender() {
gender.fr = {
...gender.fr,
Confederation: "feminine",
Diarchy: "feminine",
Horde: "feminine",
League: "feminine",
Marches: "feminine",
Oligarchy: "feminine",
Tetrarchy: "feminine",
Theocracy: "feminine",
"Trade Company": "feminine",
Union: "feminine",
};
}

Copilot uses AI. Check for mistakes.
@Azgaar
Copy link
Copy Markdown
Owner

Azgaar commented Apr 1, 2026

@Blipz, Can you please check the comment and also I see a lot of missing translations (empty strings) in French file. It's better not to translate then to leave an empty string.

@Blipz
Copy link
Copy Markdown
Author

Blipz commented Apr 2, 2026

@Blipz, Can you please check the comment and also I see a lot of missing translations (empty strings) in French file. It's better not to translate then to leave an empty string.

I planned to complete them eventually, but in the meantime, it helped me to know what remains to be translated (i18next is configured to use the English fallback when the translation is empty).

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.

3 participants