chore: heygermany customer project
This commit is contained in:
106
apps/website/i18n/config.ts
Normal file
106
apps/website/i18n/config.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { useSyncExternalStore } from 'react'
|
||||
import { DEFAULT_LOCALE, LOCALES, type SupportedLocale } from '@/config'
|
||||
|
||||
export const supportedLocales = LOCALES
|
||||
export const defaultLocale = DEFAULT_LOCALE
|
||||
export type Locale = SupportedLocale
|
||||
|
||||
const LANGUAGE_STORAGE_KEY = 'heygermany.language'
|
||||
const RTL_LOCALES = new Set(['ar', 'he', 'fa', 'ur'])
|
||||
|
||||
let activeLocale: Locale = defaultLocale
|
||||
const listeners = new Set<() => void>()
|
||||
|
||||
export const getLocale = (): Locale => activeLocale
|
||||
|
||||
export function localeDir(locale: string = defaultLocale): 'rtl' | 'ltr' {
|
||||
return RTL_LOCALES.has(locale.split('-')[0]) ? 'rtl' : 'ltr'
|
||||
}
|
||||
|
||||
const normalizeLanguage = (value: string | null | undefined): Locale | undefined => {
|
||||
const short = value?.slice(0, 2).toLowerCase()
|
||||
return supportedLocales.includes(short as Locale) ? (short as Locale) : undefined
|
||||
}
|
||||
|
||||
export function detectInitialLocale(): Locale {
|
||||
if (typeof window === 'undefined') {
|
||||
return defaultLocale
|
||||
}
|
||||
|
||||
return (
|
||||
normalizeLanguage(window.localStorage.getItem(LANGUAGE_STORAGE_KEY)) ??
|
||||
normalizeLanguage(window.navigator.language) ??
|
||||
normalizeLanguage(window.document.documentElement.lang) ??
|
||||
defaultLocale
|
||||
)
|
||||
}
|
||||
|
||||
export function setActiveLocale(next: Locale): void {
|
||||
if (next === activeLocale) {
|
||||
return
|
||||
}
|
||||
|
||||
activeLocale = next
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.localStorage.setItem(LANGUAGE_STORAGE_KEY, next)
|
||||
window.document.documentElement.lang = next
|
||||
}
|
||||
|
||||
for (const listener of listeners) {
|
||||
listener()
|
||||
}
|
||||
}
|
||||
|
||||
export function setRouteLocale(next: Locale): void {
|
||||
if (next === activeLocale) {
|
||||
return
|
||||
}
|
||||
|
||||
activeLocale = next
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.document.documentElement.lang = next
|
||||
}
|
||||
|
||||
for (const listener of listeners) {
|
||||
listener()
|
||||
}
|
||||
}
|
||||
|
||||
function subscribe(listener: () => void): () => void {
|
||||
listeners.add(listener)
|
||||
return () => listeners.delete(listener)
|
||||
}
|
||||
|
||||
activeLocale = detectInitialLocale()
|
||||
|
||||
export function useLocale(): { locale: Locale; dir: 'ltr' | 'rtl'; setLocale: (locale: Locale) => void } {
|
||||
const locale = useSyncExternalStore<Locale>(subscribe, () => activeLocale, () => activeLocale)
|
||||
return {
|
||||
locale,
|
||||
dir: localeDir(locale),
|
||||
setLocale: setActiveLocale,
|
||||
}
|
||||
}
|
||||
|
||||
export function isI18nDebug(): boolean {
|
||||
if (typeof process !== 'undefined' && process.env?.I18N_DEBUG === '1') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (typeof window === 'undefined') {
|
||||
return false
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
if (params.has('i18n-debug')) {
|
||||
return params.get('i18n-debug') !== '0'
|
||||
}
|
||||
|
||||
return window.localStorage?.getItem('i18n-debug') === '1'
|
||||
}
|
||||
|
||||
export function maskI18n(value: string): string {
|
||||
return isI18nDebug() ? value.replace(/\S/g, '█') : value
|
||||
}
|
||||
12
apps/website/i18n/ident.ts
Normal file
12
apps/website/i18n/ident.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
// Single source of truth for turning an i18next dotted key (`landing.cards.x`)
|
||||
// into a Paraglide snake_case message identifier (`landing_cards_x`). Used by the
|
||||
// runtime resolver `mKey` for the unavoidable dynamic cases.
|
||||
export const identOf = (dotPath: string): string =>
|
||||
dotPath
|
||||
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
||||
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
|
||||
.replace(/[.\-\s]+/g, '_')
|
||||
.replace(/[^A-Za-z0-9_]/g, '')
|
||||
.replace(/_+/g, '_')
|
||||
.replace(/^_+|_+$/g, '')
|
||||
.toLowerCase()
|
||||
128
apps/website/i18n/messages.ts
Normal file
128
apps/website/i18n/messages.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import type { I18nString } from '@pikku/react'
|
||||
import arMessages from '../messages/ar.json'
|
||||
import bsMessages from '../messages/bs.json'
|
||||
import deMessages from '../messages/de.json'
|
||||
import enMessages from '../messages/en.json'
|
||||
import esMessages from '../messages/es.json'
|
||||
import filMessages from '../messages/fil.json'
|
||||
import hiMessages from '../messages/hi.json'
|
||||
import sqMessages from '../messages/sq.json'
|
||||
import trMessages from '../messages/tr.json'
|
||||
import ukMessages from '../messages/uk.json'
|
||||
import viMessages from '../messages/vi.json'
|
||||
import { defaultLocale, getLocale, maskI18n, type Locale } from './config'
|
||||
import { identOf } from './ident'
|
||||
|
||||
type MessageCatalog = Record<string, string>
|
||||
type MessageArgs = Record<string, unknown>
|
||||
type MessageFn = (args?: MessageArgs) => I18nString
|
||||
|
||||
const catalogs: Record<Locale, MessageCatalog> = {
|
||||
ar: arMessages as MessageCatalog,
|
||||
bs: bsMessages as MessageCatalog,
|
||||
de: deMessages as MessageCatalog,
|
||||
en: enMessages as MessageCatalog,
|
||||
es: esMessages as MessageCatalog,
|
||||
fil: filMessages as MessageCatalog,
|
||||
hi: hiMessages as MessageCatalog,
|
||||
sq: sqMessages as MessageCatalog,
|
||||
tr: trMessages as MessageCatalog,
|
||||
uk: ukMessages as MessageCatalog,
|
||||
vi: viMessages as MessageCatalog,
|
||||
}
|
||||
|
||||
const interpolate = (template: string, args?: MessageArgs) =>
|
||||
template.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_match, key: string) => {
|
||||
const value = args?.[key]
|
||||
return value === undefined || value === null ? '' : String(value)
|
||||
})
|
||||
|
||||
const getMessageValue = (key: string, locale = getLocale()) =>
|
||||
catalogs[locale]?.[key] ?? catalogs[defaultLocale]?.[key]
|
||||
|
||||
const toI18n = (value: string) => maskI18n(value) as unknown as I18nString
|
||||
|
||||
const parseListValue = (value: string): string[] => {
|
||||
try {
|
||||
const parsed = JSON.parse(value) as unknown
|
||||
if (Array.isArray(parsed)) {
|
||||
return parsed.map((item) => String(item))
|
||||
}
|
||||
} catch {
|
||||
// Fall through to a permissive parser for malformed copied JSON.
|
||||
}
|
||||
|
||||
return value
|
||||
.replace(/^\s*\[/, '')
|
||||
.replace(/\]\s*$/, '')
|
||||
.split(/","|"،\s*"|»,\s*«|",\s*"/)
|
||||
.map((item) => item.replace(/^["«\s]+|["»\s]+$/g, '').trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
const getIndexedList = (keyPrefix: string): string[] => {
|
||||
const values: string[] = []
|
||||
for (let index = 0; ; index += 1) {
|
||||
const value = getMessageValue(`${keyPrefix}_${index}`)
|
||||
if (!value) {
|
||||
break
|
||||
}
|
||||
values.push(value)
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
export const mKey = (key: string, args?: MessageArgs): I18nString => {
|
||||
const value = getMessageValue(identOf(key))
|
||||
if (!value) {
|
||||
if (typeof console !== 'undefined') {
|
||||
console.warn(`[i18n] missing message for key "${key}"`)
|
||||
}
|
||||
return toI18n(key)
|
||||
}
|
||||
|
||||
return toI18n(interpolate(value, args))
|
||||
}
|
||||
|
||||
// Plain-string resolution in an explicit locale (no active-locale store).
|
||||
// For server-side, locale-parameterized text like SEO meta tags.
|
||||
export const mTextIn = (
|
||||
locale: Locale,
|
||||
key: string,
|
||||
args?: MessageArgs,
|
||||
): string => {
|
||||
const value = getMessageValue(identOf(key), locale)
|
||||
return value ? interpolate(value, args) : key
|
||||
}
|
||||
|
||||
export const mList = (keyPrefix: string, args?: MessageArgs): I18nString[] => {
|
||||
const ident = identOf(keyPrefix)
|
||||
const indexedValues = getIndexedList(ident)
|
||||
const values =
|
||||
indexedValues.length > 0
|
||||
? indexedValues
|
||||
: (() => {
|
||||
const exactValue = getMessageValue(ident)
|
||||
return exactValue ? parseListValue(exactValue) : []
|
||||
})()
|
||||
|
||||
return values.map((value) => toI18n(interpolate(value, args)))
|
||||
}
|
||||
|
||||
export const m = new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_target, property) => {
|
||||
if (typeof property !== 'string') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const fn: MessageFn = (args) => {
|
||||
const value = getMessageValue(property)
|
||||
return toI18n(interpolate(value ?? property, args))
|
||||
}
|
||||
|
||||
return fn
|
||||
},
|
||||
},
|
||||
) as Record<string, MessageFn>
|
||||
Reference in New Issue
Block a user