diff --git a/packages/core/lang/en.json b/packages/core/lang/en.json index 0439d8d0e..8d5244363 100644 --- a/packages/core/lang/en.json +++ b/packages/core/lang/en.json @@ -1,5 +1,18 @@ { + "test": { + "$": "test", + "long": "This is a long test string. You MUST NEVER see this in the UI. My favorite fruit is %s1 because it has the color of %s2. I like it so much that I will say its name 3 times! %s1! %s1! %s1! Once again, it's %s2, not %s3.", + "fox": "The quick brown fox jumps over the lazy dog." + }, + "common": { + "branches": { + "local": "dev", + "dev": "beta", + "opentest": "opentest", + "preview": "preview" + }, + "locales": { "en": "English", "es": "Spanish", @@ -689,5 +702,129 @@ } }, - "website": {} + "website": { + "home": { + "hero": { + "credit": "By %s1 ❤️" + }, + "hot_tanks": { + "title": "Popular tanks", + "subtitle": "Last %s1 days" + }, + "other_projects": { + "title": "Other cool projects" + } + }, + + "tools": { + "tankopedia": { + "name": "Tankopedia", + "button": "Find tanks", + "description": "Blitz tank encyclopedia", + "seo_description": "Statistics, armor, and more for all tanks in World of Tanks Blitz" + }, + "compare": { + "name": "Compare", + "button": "Compare tanks", + "description": "Compare tank statistics", + "seo_description": "Compare tanks statistics and loadouts in World of Tanks Blitz" + }, + "performance": { + "name": "Performance", + "button": "View tanks", + "description": "Tank performance statistics", + "seo_description": "Best tanks and their performances in World of Tanks Blitz" + }, + "gallery": { + "name": "Gallery", + "button": "Browse all", + "description": "Blitz player avatars", + "seo_description": "All World of Tanks Blitz player avatars" + }, + "session": { + "name": "Session", + "button": "Start tracking", + "description": "Live session stats", + "seo_description": "Track your stats in real time as you play World of Tanks Blitz" + }, + "tier_list": { + "name": "Tier list", + "button": "Rank tanks", + "description": "Rank Blitz tanks", + "seo_description": "Explore tier lists ranking tanks from best to worst, based on player ratings and statistical performance in World of Tanks Blitz" + }, + "embed": { + "name": "Embed", + "button": "Create now", + "description": "Embeds for streaming", + "seo_description": "Create embeds for streaming World of Tanks Blitz with extreme customization" + }, + "charts": { + "name": "Charts", + "button": "Nerd out", + "description": "Lots of graphs", + "seo_description": "Charts and graphs on anything and everything about tanks, players, and events in World of Tanks Blitz" + }, + "rating": { + "name": "Rating", + "button": "View leaderboard", + "description": "Full rating leaderboard", + "seo_description": "Live and archived full rating leaderboard for World of Tanks Blitz" + }, + "discord": { + "name": "Discord bot", + "button": "Install now", + "description": "Stats right in Discord", + "seo_description": "Get stats right in Discord with BlitzKit's Discord bot" + }, + "more": { + "name": "More coming soon", + "button": "Join Discord", + "description": "What we're making", + "seo_description": "What we're making with BlitzKit" + }, + "players": { + "name": "Players", + "button": "Lookup player", + "description": "Periodical player statistics", + "seo_description": "Periodical player statistics in World of Tanks Blitz" + }, + "home": { + "name": "Home", + "button": "Get started", + "description": "Get started with BlitzKit", + "seo_description": "Get started with BlitzKit" + }, + "blitz_analysis": { + "name": "BlitzAnalysis[]", + "button": "Analyze now", + "description": "Rich historic average stats", + "seo_description": "Rich historic average stats in World of Tanks Blitz" + }, + "blitz_insider": { + "name": "Blitz Insider", + "button": "Discover today", + "description": "In-game session stats", + "seo_description": "In-game session stats in World of Tanks Blitz" + } + }, + + "footer": { + "motto": "Everything World of Tanks Blitz", + "copyright": "Unaffiliated with Wargaming or any service", + + "about": { + "title": "About", + "credits": "Credits", + "discord": "Discord", + "github": "GitHub", + "patches": "Patches" + }, + "legal": { + "title": "Legal", + "privacy": "Privacy policy", + "terms": "Terms of service" + } + } + } } diff --git a/packages/core/src/blitzkit/supportedLocales.ts b/packages/core/src/blitzkit/supportedLocales.ts index 864646de8..5e5465c48 100644 --- a/packages/core/src/blitzkit/supportedLocales.ts +++ b/packages/core/src/blitzkit/supportedLocales.ts @@ -1,13 +1,2 @@ -export const SUPPORTED_LOCALES = [ - 'en', - 'es', - 'hr', - 'pt', - 'ru', - 'uk', - 'zh', -] as const; - +export const SUPPORTED_LOCALES = ['en', 'es', 'hr', 'pt', 'ru', 'uk', 'zh']; export const DEFAULT_LOCALE = SUPPORTED_LOCALES[0]; - -export type SupportedLocale = (typeof SUPPORTED_LOCALES)[number]; diff --git a/packages/core/src/i18n/index.ts b/packages/core/src/i18n/index.ts new file mode 100644 index 000000000..d74dfb75c --- /dev/null +++ b/packages/core/src/i18n/index.ts @@ -0,0 +1,2 @@ +export * from './strings'; +export * from './translator'; diff --git a/packages/core/src/i18n/strings.ts b/packages/core/src/i18n/strings.ts new file mode 100644 index 000000000..7a0ec0c32 --- /dev/null +++ b/packages/core/src/i18n/strings.ts @@ -0,0 +1,46 @@ +import { merge } from 'lodash-es'; +import type en from '../../lang/en.json'; +import { DEFAULT_LOCALE, SUPPORTED_LOCALES } from '../blitzkit'; +import { TranslationTree } from './translator'; + +const DEBUG_MISSING_I18N = import.meta.env.DEBUG_MISSING_I18N; +const files = import.meta.glob('../../lang/*.json', { eager: true }); +export const localizedStrings: Record = {}; + +function nuke(object: TranslationTree) { + for (const key in object) { + const typedKey = key as keyof typeof object; + + if (typeof object[typedKey] === 'string') { + const value = object[typedKey]; + object[typedKey] = redact(value as string); + } else nuke(object[typedKey] as TranslationTree); + } +} + +SUPPORTED_LOCALES.forEach((locale) => { + const defaultStrings = files[`../../lang/${DEFAULT_LOCALE}.json`]; + const strings = files[`../../lang/${locale}.json`]; + const mergedStrings = merge({}, defaultStrings, strings); + + if (DEBUG_MISSING_I18N === 'true') nuke(mergedStrings); + + localizedStrings[locale] = mergedStrings as typeof en; +}); + +function redact(string: string) { + const matches = [...string.matchAll(/%s\d+/g)]; + const characters = string + .split('') + .map((char) => (char === ' ' ? ' ' : '█')) as string[]; + + matches.forEach((match) => { + const start = match.index; + const matchText = match[0]; + for (let i = 0; i < matchText.length; i++) { + characters[start + i] = matchText[i]; + } + }); + + return characters.join(''); +} diff --git a/packages/prate/src/index.ts b/packages/core/src/i18n/translator.ts similarity index 86% rename from packages/prate/src/index.ts rename to packages/core/src/i18n/translator.ts index 9e4a8e154..742431c49 100644 --- a/packages/prate/src/index.ts +++ b/packages/core/src/i18n/translator.ts @@ -1,9 +1,6 @@ -import en from '@blitzkit/core/lang/en.json'; -import ru from '@blitzkit/core/lang/ru.json'; +export type TranslationTree = { [key: string]: TranslationNode }; -type TranslationTree = { [key: string]: Translation }; - -type Translation = string | TranslationTree; +export type TranslationNode = string | TranslationTree; type NestedKeys = Type extends object ? { @@ -17,7 +14,7 @@ type NestedKeys = Type extends object }[keyof Type] : never; -class Prate< +export class Translator< Locale extends string, DefaultLocale extends Locale, Strings extends TranslationTree, @@ -26,8 +23,8 @@ class Prate< private readonly defaultStrings: Map; constructor( - public strings: Record, - public defaultLocale: DefaultLocale, + private strings: Record, + private defaultLocale: DefaultLocale, ) { this.defaultStrings = this.flatten(strings[defaultLocale]); } @@ -35,7 +32,7 @@ class Prate< private flatten(strings: TranslationTree) { const translations = new Map(); - function traverse(parent: string, tree: Record) { + function traverse(parent: string, tree: Record) { for (const key in tree) { const path = `${parent}${key}`; @@ -91,5 +88,3 @@ class Prate< return { translate, t: translate }; } } - -const prate = new Prate({ en, ru }, 'en'); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index cae496f70..6dc0f8de4 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,6 +1,7 @@ export * from './blitz'; export * from './blitzkit'; export * from './databases'; +export * from './i18n'; export * from './imgur'; export * from './math'; export * from './patreon'; diff --git a/packages/core/src/vite-env.d.ts b/packages/core/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/packages/core/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/website/src/components/Footer.astro b/packages/website/src/components/Footer.astro index 1f22b5a93..8518e0db3 100644 --- a/packages/website/src/components/Footer.astro +++ b/packages/website/src/components/Footer.astro @@ -2,10 +2,12 @@ import { assertSecret, fetchGameDefinitions } from '@blitzkit/core'; import { Code, Flex, Heading, Link, Text } from '@radix-ui/themes'; import packageJson from '../../../../package.json'; +import { getStrings } from '../core/i18n/getStrings'; import { LinkI18n } from './LinkI18n'; const gameDefinitions = await fetchGameDefinitions(); const { locale } = Astro.params; +const strings = getStrings(locale); --- - {locale} BlitzKit - Everything World of Tanks Blitz + {strings.website.footer.motto} - Unaffiliated with Wargaming or any service + {strings.website.footer.copyright} @@ -63,7 +64,7 @@ const { locale } = Astro.params; sm: 'auto', }} > - About + {strings.website.footer.about.title} - Credits + + {strings.website.footer.about.credits} + - Discord + {strings.website.footer.about.discord} - GitHub + {strings.website.footer.about.github} - Patches + + {strings.website.footer.about.patches} + @@ -105,13 +107,13 @@ const { locale } = Astro.params; sm: 'auto', }} > - Legal + {strings.website.footer.legal.title} - Privacy policy + {strings.website.footer.legal.privacy} - Terms of service + {strings.website.footer.legal.terms} diff --git a/packages/website/src/components/HomeHero.astro b/packages/website/src/components/HomeHero.astro index 06d7c0308..424cfbe87 100644 --- a/packages/website/src/components/HomeHero.astro +++ b/packages/website/src/components/HomeHero.astro @@ -1,9 +1,14 @@ --- import { Code, Flex, Heading, Text } from '@radix-ui/themes'; import { resolveBranchName } from '../core/blitzkit/branches'; +import { DEFAULT_LOCALE } from '@blitzkit/core'; +import { getStrings } from '../core/i18n/getStrings'; +import { literals } from '../core/i18n/literals'; -const branchName = resolveBranchName(); +const { locale } = Astro.params; +const branchName = resolveBranchName(locale); const isBranchNamed = branchName !== undefined; +const strings = getStrings(locale); ---