@jjlmoya/utils-science 1.53.0 → 1.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/package.json +18 -5
  2. package/scripts/postbuild.mjs +17 -0
  3. package/scripts/postinstall.mjs +2 -4
  4. package/src/components/ProductionBreadcrumb.astro +134 -0
  5. package/src/components/ProductionStructuredData.astro +46 -0
  6. package/src/components/ProductionWidget.astro +182 -0
  7. package/src/i18n/header-ui.ts +15 -0
  8. package/src/i18n/language-ui.ts +9 -0
  9. package/src/i18n/languages.ts +24 -0
  10. package/src/identity/brands.ts +5 -0
  11. package/src/layouts/ProductionCategoryPage.astro +184 -0
  12. package/src/layouts/ProductionPage.astro +218 -0
  13. package/src/layouts/ProductionUtilityPage.astro +283 -0
  14. package/src/mfe/assets.ts +39 -0
  15. package/src/mfe/category-ui.ts +34 -0
  16. package/src/mfe/manifest.ts +45 -0
  17. package/src/mfe/routes.ts +50 -0
  18. package/src/pages/[locale]/[utilities]/[categories]/[category]/[slug]/manifest.json.ts +49 -0
  19. package/src/pages/[locale]/[utilities]/[categories]/[category]/[slug].astro +100 -0
  20. package/src/pages/[locale]/[utilities]/[categories]/[category].astro +44 -0
  21. package/src/pages/index.astro +1 -2
  22. package/src/pages/mfe-sitemaps/[locale]/[vertical]/sitemap.xml.ts +75 -0
  23. package/src/pages/utilidades/[slug]/manifest.json.ts +28 -0
  24. package/src/pages/utilidades/[slug].astro +90 -0
  25. package/src/pages/utilidades/categorias/[category].astro +38 -0
  26. package/src/tests/diacritics_density.test.ts +1 -1
  27. package/src/tests/inverted_punctuation.test.ts +1 -1
  28. package/src/tests/mfe_cache_contract.test.ts +14 -0
  29. package/src/tests/mfe_manifest.test.ts +28 -0
  30. package/src/tests/registry_contract.test.ts +67 -0
  31. package/src/tests/script_density.test.ts +1 -1
  32. package/src/tests/seo_translation_completeness.test.ts +3 -4
  33. package/src/tests/translation_copy.test.ts +2 -2
  34. package/src/types.ts +5 -7
  35. package/src/worker.ts +27 -0
  36. package/src/pages/[locale]/[slug].astro +0 -162
  37. package/src/pages/[locale].astro +0 -251
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-science",
3
- "version": "1.53.0",
3
+ "version": "1.55.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -33,7 +33,17 @@
33
33
  "patch": "npm version patch",
34
34
  "minor": "npm version minor",
35
35
  "major": "npm version major",
36
- "postinstall": "node scripts/postinstall.mjs"
36
+ "postinstall": "node scripts/postinstall.mjs",
37
+ "predev": "node scripts/postinstall.mjs",
38
+ "prestart": "node scripts/postinstall.mjs",
39
+ "prebuild": "node scripts/postinstall.mjs",
40
+ "postbuild": "node scripts/postbuild.mjs",
41
+ "qa": "npm run lint && npm run test && npm run build",
42
+ "cf:dry-run": "npm run build && wrangler deploy --dry-run",
43
+ "cf:preview": "npm run build && wrangler deploy --config wrangler.staging.jsonc",
44
+ "cf:deploy": "npm run build && wrangler deploy",
45
+ "cf:deployments": "wrangler deployments list",
46
+ "cf:rollback": "wrangler rollback"
37
47
  },
38
48
  "lint-staged": {
39
49
  "*.{ts,tsx,astro}": [
@@ -43,9 +53,11 @@
43
53
  "dependencies": {
44
54
  "@iconify-json/mdi": "^1.2.3",
45
55
  "@jjlmoya/prompagate": "^1.1.0",
46
- "@jjlmoya/utils-shared": "1.2.0",
56
+ "@jjlmoya/utils-shared": "1.5.0",
47
57
  "astro": "^6.1.2",
48
- "astro-icon": "^1.1.0"
58
+ "astro-icon": "^1.1.0",
59
+ "@jjlmoya/identity": "0.4.0",
60
+ "@jjlmoya/tracking": "0.2.0"
49
61
  },
50
62
  "peerDependencies": {
51
63
  "leaflet": "^1.9.4"
@@ -66,6 +78,7 @@
66
78
  "stylelint-declaration-strict-value": "^1.11.1",
67
79
  "typescript": "^5.4.0",
68
80
  "typescript-eslint": "^8.58.0",
69
- "vitest": "^4.1.2"
81
+ "vitest": "^4.1.2",
82
+ "wrangler": "4.113.0"
70
83
  }
71
84
  }
@@ -0,0 +1,17 @@
1
+ import { copyFileSync, existsSync, mkdirSync, rmSync, readdirSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+
4
+ const sourceRoot = resolve('dist/mfe-sitemaps');
5
+ const destinationRoot = resolve('dist/_utilities');
6
+
7
+ if (existsSync(sourceRoot)) {
8
+ for (const locale of readdirSync(sourceRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory())) {
9
+ for (const vertical of readdirSync(join(sourceRoot, locale.name), { withFileTypes: true }).filter((entry) => entry.isDirectory())) {
10
+ const source = join(sourceRoot, locale.name, vertical.name, 'sitemap.xml');
11
+ const destinationDirectory = join(destinationRoot, locale.name, vertical.name);
12
+ mkdirSync(destinationDirectory, { recursive: true });
13
+ copyFileSync(source, join(destinationDirectory, 'sitemap.xml'));
14
+ }
15
+ }
16
+ rmSync(sourceRoot, { recursive: true, force: true });
17
+ }
@@ -6,11 +6,9 @@ const libDir = dirname(fileURLToPath(import.meta.url));
6
6
  const toolsDir = join(libDir, '../src/tool');
7
7
 
8
8
  const inNodeModules = libDir.includes('node_modules');
9
- if (!inNodeModules) process.exit(0);
10
-
11
- const projectRoot = join(libDir, '../../../..');
9
+ const projectRoot = inNodeModules ? join(libDir, '../../../..') : join(libDir, '..');
12
10
  const categoryKey = JSON.parse(readFileSync(join(libDir, '../package.json'), 'utf8')).name.replace('@jjlmoya/utils-', '');
13
- const destDir = join(projectRoot, `public/styles/lib/${categoryKey}`);
11
+ const destDir = join(projectRoot, `public/_utilities/${categoryKey}/styles`);
14
12
 
15
13
  mkdirSync(destDir, { recursive: true });
16
14
 
@@ -0,0 +1,134 @@
1
+ ---
2
+ import { Icon } from "astro-icon/components";
3
+ import type { KnownLocale } from "../types";
4
+ import { getCategoryUi } from "../mfe/category-ui";
5
+ import { getUtilitiesPath } from "@jjlmoya/utils-shared/routing";
6
+
7
+ interface Props {
8
+ locale: KnownLocale;
9
+ categoryTitle?: string;
10
+ categoryHref?: string;
11
+ currentTitle?: string;
12
+ }
13
+
14
+ const LABELS: Record<KnownLocale, string> = {
15
+ es: "Todas las utilidades",
16
+ en: "All utilities",
17
+ fr: "Toutes les utilités",
18
+ de: "Alle Werkzeuge",
19
+ it: "Tutte le utilità",
20
+ pt: "Todas as utilidades",
21
+ nl: "Alle hulpmiddelen",
22
+ sv: "Alla verktyg",
23
+ pl: "Wszystkie narzędzia",
24
+ id: "Semua utilitas",
25
+ tr: "Tüm araçlar",
26
+ ru: "Все инструменты",
27
+ ja: "すべてのユーティリティ",
28
+ ko: "모든 유틸리티",
29
+ zh: "所有工具",
30
+ };
31
+
32
+ const { locale, categoryTitle, categoryHref, currentTitle } = Astro.props;
33
+ const homeLabel = LABELS[locale];
34
+ const breadcrumbLabel = getCategoryUi(locale).breadcrumb;
35
+ ---
36
+
37
+ <nav class="production-breadcrumb" aria-label={breadcrumbLabel}>
38
+ <ol class="production-breadcrumb-list">
39
+ <li class="production-breadcrumb-item">
40
+ <a href={getUtilitiesPath(locale)}>{homeLabel}</a>
41
+ </li>
42
+ {categoryTitle && categoryHref && (
43
+ <>
44
+ <li class="production-breadcrumb-separator" aria-hidden="true">
45
+ <Icon name="mdi:chevron-right" />
46
+ </li>
47
+ <li class="production-breadcrumb-item">
48
+ <a href={categoryHref}>{categoryTitle}</a>
49
+ </li>
50
+ </>
51
+ )}
52
+ {currentTitle && (
53
+ <>
54
+ <li class="production-breadcrumb-separator" aria-hidden="true">
55
+ <Icon name="mdi:chevron-right" />
56
+ </li>
57
+ <li class="production-breadcrumb-item production-breadcrumb-current" aria-current="page">
58
+ <span>{currentTitle}</span>
59
+ </li>
60
+ </>
61
+ )}
62
+ </ol>
63
+ </nav>
64
+
65
+ <style>
66
+ .production-breadcrumb {
67
+ margin-bottom: var(--space-xl);
68
+ }
69
+
70
+ .production-breadcrumb-list {
71
+ display: flex;
72
+ flex-wrap: wrap;
73
+ gap: var(--space-sm);
74
+ align-items: center;
75
+ margin: 0;
76
+ padding: 0;
77
+ list-style: none;
78
+ }
79
+
80
+ .production-breadcrumb-item {
81
+ display: flex;
82
+ min-width: 0;
83
+ align-items: center;
84
+ color: var(--text-muted);
85
+ font-size: 0.9rem;
86
+ }
87
+
88
+ .production-breadcrumb-item a {
89
+ overflow: hidden;
90
+ color: var(--text-main);
91
+ font-weight: 500;
92
+ text-decoration: none;
93
+ text-overflow: ellipsis;
94
+ white-space: nowrap;
95
+ }
96
+
97
+ .production-breadcrumb-item a:hover,
98
+ .production-breadcrumb-item a:focus-visible {
99
+ color: var(--science-accent);
100
+ text-decoration: underline;
101
+ text-underline-offset: 0.25rem;
102
+ }
103
+
104
+ .production-breadcrumb-separator {
105
+ display: flex;
106
+ flex: 0 0 auto;
107
+ align-items: center;
108
+ color: var(--text-dimmed, var(--text-muted));
109
+ }
110
+
111
+ .production-breadcrumb-separator svg {
112
+ width: 1rem;
113
+ height: 1rem;
114
+ }
115
+
116
+ .production-breadcrumb-current {
117
+ overflow: hidden;
118
+ max-width: min(28rem, 70vw);
119
+ color: var(--text-muted);
120
+ font-size: 0.9rem;
121
+ text-overflow: ellipsis;
122
+ white-space: nowrap;
123
+ }
124
+
125
+ @media (width <= 640px) {
126
+ .production-breadcrumb {
127
+ margin-bottom: var(--space-lg);
128
+ }
129
+
130
+ .production-breadcrumb-item {
131
+ font-size: 0.8rem;
132
+ }
133
+ }
134
+ </style>
@@ -0,0 +1,46 @@
1
+ ---
2
+ import type { KnownLocale, ToolLocaleContent } from "../types";
3
+ import { TABLETOP_ASSET_VERSION } from "../mfe/assets";
4
+
5
+ interface Props {
6
+ locale: KnownLocale;
7
+ content: ToolLocaleContent;
8
+ publicPath: string;
9
+ categoryTitle: string;
10
+ categoryHref: string;
11
+ }
12
+
13
+ const { locale, content, publicPath, categoryTitle, categoryHref } = Astro.props;
14
+ const siteOrigin = locale === "es" ? "https://www.jjlmoya.es" : "https://www.gamebob.dev";
15
+ const fullUrl = new URL(publicPath, siteOrigin).href;
16
+ const categoryUrl = new URL(categoryHref, siteOrigin).href;
17
+ const brandName = locale === "es" ? "jjlmoya" : "GameBob";
18
+ const schemas = (content.schemas ?? []).filter((schema) => (schema as unknown as Record<string, unknown>)["@type"] !== "FAQPage");
19
+ const schemaTypes = new Set(schemas.map((schema) => (schema as unknown as Record<string, unknown>)["@type"]));
20
+ const breadcrumbSchema = {
21
+ "@context": "https://schema.org",
22
+ "@type": "BreadcrumbList",
23
+ itemListElement: [
24
+ { "@type": "ListItem", position: 1, name: brandName, item: `${siteOrigin}/${locale === "es" ? "" : `${locale}/`}` },
25
+ { "@type": "ListItem", position: 2, name: categoryTitle, item: categoryUrl },
26
+ { "@type": "ListItem", position: 3, name: content.title, item: fullUrl },
27
+ ],
28
+ };
29
+ const webApplicationSchema = {
30
+ "@context": "https://schema.org",
31
+ "@type": "WebApplication",
32
+ name: content.title,
33
+ description: content.description,
34
+ url: fullUrl,
35
+ applicationCategory: "UtilityApplication",
36
+ operatingSystem: "Any",
37
+ softwareVersion: TABLETOP_ASSET_VERSION,
38
+ author: { "@type": "Person", name: "jjlmoya", url: siteOrigin },
39
+ publisher: { "@type": "Organization", name: brandName, url: siteOrigin },
40
+ inLanguage: locale,
41
+ offers: { "@type": "Offer", price: "0", priceCurrency: "EUR" },
42
+ };
43
+ ---
44
+ <script is:inline type="application/ld+json" set:html={JSON.stringify(breadcrumbSchema)} />
45
+ {!schemaTypes.has("WebApplication") && <script is:inline type="application/ld+json" set:html={JSON.stringify(webApplicationSchema)} />}
46
+ {schemas.map((schema) => <script is:inline type="application/ld+json" set:html={JSON.stringify(schema)} />)}
@@ -0,0 +1,182 @@
1
+ ---
2
+ import { Icon } from "astro-icon/components";
3
+ import type { KnownLocale } from "../types";
4
+
5
+ interface Props {
6
+ locale: KnownLocale;
7
+ slug: string;
8
+ }
9
+
10
+ const { locale, slug } = Astro.props;
11
+
12
+ const copy: Record<KnownLocale, { badge: string; title: string; description: string; action: string }> = {
13
+ es: { badge: "Estudio de utilidades", title: "¿Quieres esta utilidad en tu web?", description: "Personaliza colores y modo oscuro para WordPress, Notion o tu propia web.", action: "Empezar" },
14
+ en: { badge: "Utilities Studio", title: "Want this utility on your website?", description: "Customize colors and dark mode for WordPress, Notion or your own site.", action: "Go Ahead" },
15
+ fr: { badge: "Studio d'utilitaires", title: "Voulez-vous cet outil sur votre site web ?", description: "Personnalisez les couleurs et le mode sombre pour WordPress, Notion ou votre propre site.", action: "Allons-y" },
16
+ de: { badge: "Tools-Studio", title: "Möchtest du dieses Werkzeug auf deiner Website?", description: "Passe Farben und den Dunkelmodus für WordPress, Notion oder deine eigene Website an.", action: "Los geht's" },
17
+ it: { badge: "Studio Strumenti", title: "Vuoi questo strumento sul tuo sito?", description: "Personalizza i colori e la modalità scura per WordPress, Notion o il tuo sito.", action: "Vai" },
18
+ pt: { badge: "Estúdio de Utilitários", title: "Quer este utilitário no seu site?", description: "Personalize cores e o modo escuro para WordPress, Notion ou o seu próprio site.", action: "Avançar" },
19
+ nl: { badge: "Hulpmiddelenstudio", title: "Wil je dit hulpmiddel op je website?", description: "Pas kleuren en de donkere modus aan voor WordPress, Notion of je eigen site.", action: "Ga verder" },
20
+ sv: { badge: "Verktygsstudio", title: "Vill du ha det här verktyget på din webbplats?", description: "Anpassa färger och mörkt läge för WordPress, Notion eller din egen sajt.", action: "Kör igång" },
21
+ pl: { badge: "Studio narzędzi", title: "Chcesz mieć to narzędzie na swojej stronie?", description: "Dostosuj kolory i tryb ciemny dla WordPress, Notion lub własnej witryny.", action: "Działaj" },
22
+ id: { badge: "Studio Utilitas", title: "Ingin utilitas ini di situs Anda?", description: "Sesuaikan warna dan mode gelap untuk WordPress, Notion, atau situs Anda sendiri.", action: "Mulai Sekarang" },
23
+ tr: { badge: "Araç Stüdyosu", title: "Bu aracı kendi sitenizde kullanmak ister misiniz?", description: "WordPress, Notion veya kendi siteniz için renkleri ve karanlık modu özelleştirin.", action: "Hemen Başla" },
24
+ ru: { badge: "Студия утилит", title: "Хотите эту утилиту на своём сайте?", description: "Настройте цвета и тёмную тему для WordPress, Notion или вашего сайта.", action: "Вперёд" },
25
+ ja: { badge: "ユーティリティスタジオ", title: "このユーティリティをあなたのサイトに追加しませんか?", description: "WordPress、Notion、またはご自身のサイト向けに、カラーとダークモードをカスタマイズできます。", action: "さっそく始める" },
26
+ ko: { badge: "유틸리티 스튜디오", title: "이 유틸리티를 내 웹사이트에 추가하고 싶으신가요?", description: "WordPress, Notion 또는 직접 운영하는 사이트에 맞게 색상과 다크 모드를 설정하세요.", action: "시작하기" },
27
+ zh: { badge: "工具工作室", title: "想把这个工具添加到你的网站吗?", description: "为 WordPress、Notion 或你自己的网站自定义配色和深色模式。", action: "立即开始" },
28
+ };
29
+
30
+ const text = copy[locale];
31
+ const widgetPath = locale === "es" ? `/widgets/?utilidad=${encodeURIComponent(slug)}` : `/${locale}/widgets/?utilidad=${encodeURIComponent(slug)}`;
32
+ const widgetHref = locale === "es" ? `https://www.jjlmoya.es${widgetPath}` : `https://www.gamebob.dev${widgetPath}`;
33
+ ---
34
+
35
+ <section class="production-widget" aria-labelledby="production-widget-title">
36
+ <div class="production-widget-card">
37
+ <div class="production-widget-glow"></div>
38
+ <div class="production-widget-content">
39
+ <div class="production-widget-copy">
40
+ <span class="production-widget-badge"><Icon name="mdi:widgets-outline" />{text.badge}</span>
41
+ <div>
42
+ <h2 id="production-widget-title">{text.title}</h2>
43
+ <p>{text.description}</p>
44
+ </div>
45
+ </div>
46
+ <a class="production-widget-action" href={widgetHref}>
47
+ {text.action}<Icon name="mdi:arrow-right" />
48
+ </a>
49
+ </div>
50
+ </div>
51
+ </section>
52
+
53
+ <style>
54
+ .production-widget {
55
+ margin-top: var(--space-2xl);
56
+ }
57
+
58
+ .production-widget-card {
59
+ position: relative;
60
+ max-width: 56rem;
61
+ margin: 0 auto;
62
+ overflow: hidden;
63
+ padding: var(--space-md) var(--space-lg);
64
+ border: 1px solid var(--border-base);
65
+ border-radius: 1rem;
66
+ background: var(--bg-surface);
67
+ box-shadow: 0 0.75rem 2rem color-mix(in srgb, var(--shadow-base, #000) 25%, transparent);
68
+ }
69
+
70
+ .production-widget-glow {
71
+ position: absolute;
72
+ top: -3rem;
73
+ right: -3rem;
74
+ width: 8rem;
75
+ height: 8rem;
76
+ border-radius: 50%;
77
+ background: var(--science-accent);
78
+ opacity: 0.08;
79
+ filter: blur(2.5rem);
80
+ }
81
+
82
+ .production-widget-content {
83
+ position: relative;
84
+ z-index: 1;
85
+ display: flex;
86
+ align-items: center;
87
+ justify-content: space-between;
88
+ gap: var(--space-lg);
89
+ }
90
+
91
+ .production-widget-copy {
92
+ display: flex;
93
+ min-width: 0;
94
+ align-items: center;
95
+ gap: var(--space-md);
96
+ }
97
+
98
+ .production-widget-badge {
99
+ display: inline-flex;
100
+ flex-shrink: 0;
101
+ align-items: center;
102
+ gap: 0.35rem;
103
+ padding: 0.35rem 0.6rem;
104
+ border-radius: 999px;
105
+ background: color-mix(in srgb, var(--science-accent) 12%, transparent);
106
+ color: var(--science-accent);
107
+ font-size: 0.55rem;
108
+ font-weight: 900;
109
+ letter-spacing: 0.08em;
110
+ text-transform: uppercase;
111
+ }
112
+
113
+ .production-widget-badge svg,
114
+ .production-widget-action svg {
115
+ width: 0.9rem;
116
+ height: 0.9rem;
117
+ }
118
+
119
+ .production-widget-copy h2 {
120
+ margin: 0;
121
+ color: var(--text-main);
122
+ font-size: 0.95rem;
123
+ font-weight: 800;
124
+ line-height: 1.2;
125
+ }
126
+
127
+ .production-widget-copy p {
128
+ margin: 0.25rem 0 0;
129
+ overflow: hidden;
130
+ color: var(--text-muted);
131
+ font-size: 0.75rem;
132
+ line-height: 1.3;
133
+ text-overflow: ellipsis;
134
+ white-space: nowrap;
135
+ }
136
+
137
+ .production-widget-action {
138
+ display: inline-flex;
139
+ flex-shrink: 0;
140
+ align-items: center;
141
+ gap: 0.35rem;
142
+ padding: 0.6rem 0.85rem;
143
+ border-radius: 0.7rem;
144
+ background: var(--science-accent);
145
+ color: var(--bg-main);
146
+ font-size: 0.75rem;
147
+ font-weight: 800;
148
+ text-decoration: none;
149
+ transition: transform 0.2s ease, filter 0.2s ease;
150
+ }
151
+
152
+ .production-widget-action:hover,
153
+ .production-widget-action:focus-visible {
154
+ filter: brightness(1.08);
155
+ transform: translateY(-1px);
156
+ }
157
+
158
+ @media (width <= 768px) {
159
+ .production-widget-card {
160
+ padding: var(--space-md);
161
+ }
162
+
163
+ .production-widget-content {
164
+ align-items: stretch;
165
+ flex-direction: column;
166
+ }
167
+
168
+ .production-widget-copy {
169
+ align-items: flex-start;
170
+ flex-direction: column;
171
+ gap: var(--space-sm);
172
+ }
173
+
174
+ .production-widget-copy p {
175
+ white-space: normal;
176
+ }
177
+
178
+ .production-widget-action {
179
+ justify-content: center;
180
+ }
181
+ }
182
+ </style>
@@ -0,0 +1,15 @@
1
+ import type { HeaderLabels } from "@jjlmoya/identity/types";
2
+ import { LANGUAGE_CODES, type Language } from "./languages";
3
+
4
+ const DEFAULT_LABELS: HeaderLabels = {
5
+ menu: "Menu",
6
+ closeMenu: "Close menu",
7
+ theme: "Theme",
8
+ switchToLight: "Switch to light theme",
9
+ switchToDark: "Switch to dark theme",
10
+ switchToSystem: "Use system theme",
11
+ };
12
+
13
+ export const HEADER_UI = Object.fromEntries(
14
+ LANGUAGE_CODES.map((language) => [language, DEFAULT_LABELS]),
15
+ ) as Record<Language, HeaderLabels>;
@@ -0,0 +1,9 @@
1
+ import { LANGUAGE_CODES, type Language } from "./languages";
2
+
3
+ export const LANGUAGE_UI = Object.fromEntries(
4
+ LANGUAGE_CODES.map((language) => [language, {
5
+ change: "Change language",
6
+ select: "Select language",
7
+ close: "Close language selector",
8
+ }]),
9
+ ) as Record<Language, { change: string; select: string; close: string }>;
@@ -0,0 +1,24 @@
1
+ import { UTILITY_LOCALES, type UtilityLocale } from "@jjlmoya/utils-shared/routing";
2
+
3
+ export const LANGUAGES = [
4
+ ["es", "Español", "es-ES"],
5
+ ["en", "English", "en-US"],
6
+ ["fr", "Français", "fr-FR"],
7
+ ["de", "Deutsch", "de-DE"],
8
+ ["it", "Italiano", "it-IT"],
9
+ ["pt", "Português", "pt-PT"],
10
+ ["nl", "Nederlands", "nl-NL"],
11
+ ["sv", "Svenska", "sv-SE"],
12
+ ["pl", "Polski", "pl-PL"],
13
+ ["id", "Bahasa Indonesia", "id-ID"],
14
+ ["tr", "Türkçe", "tr-TR"],
15
+ ["ru", "Русский", "ru-RU"],
16
+ ["ja", "日本語", "ja-JP"],
17
+ ["ko", "한국어", "ko-KR"],
18
+ ["zh", "中文", "zh-CN"],
19
+ ] as const satisfies readonly (readonly [UtilityLocale, string, string])[];
20
+
21
+ export type Language = UtilityLocale;
22
+ export const LANGUAGE_CODES = UTILITY_LOCALES;
23
+ export const LANGUAGE_NAMES = Object.fromEntries(LANGUAGES.map(([code, name]) => [code, name])) as Record<Language, string>;
24
+ export const LANGUAGE_LOCALES = Object.fromEntries(LANGUAGES.map(([code, , locale]) => [code, locale])) as Record<Language, string>;
@@ -0,0 +1,5 @@
1
+ import type { Language } from "../i18n/languages";
2
+ import type { Identity } from "@jjlmoya/identity/types";
3
+
4
+ export type Brand = Identity;
5
+ export const getBrand = (language: Language): Brand => language === "es" ? "jjlmoya" : "gamebob";