Agent Skills: Language Specialist

|

UncategorizedID: Spectaculous-Code/raamattu-nyt/language-specialist

Install this agent skill to your local

pnpm dlx add-skill https://github.com/Spectaculous-Code/raamattu-nyt/tree/HEAD/.claude/skills/language-specialist

Skill Files

Browse the full folder contents for language-specialist.

Download Skill

Loading file tree…

.claude/skills/language-specialist/SKILL.md

Skill Metadata

Name
language-specialist
Description
|

Language Specialist

Expert assistant for all i18n/l10n work in the Raamattu Nyt project.

Architecture Overview

packages/shared-i18n/          # Shared i18n package
├── src/
│   ├── config.ts              # i18next initialization
│   ├── types.ts               # SupportedLanguage, LANGUAGE_INFO
│   ├── provider.tsx           # I18nProvider component
│   ├── hooks/useLanguage.ts   # Language preference hook
│   └── utils/
│       ├── setLanguage.ts     # Simple language switcher
│       └── dateLocale.ts      # date-fns locale helper

apps/raamattu-nyt/public/locales/   # Translation files (lazy-loaded)
├── fi/
│   ├── common.json            # Shared UI strings
│   └── profile.json           # Profile page strings
└── en/
    ├── common.json
    └── profile.json

Quick Reference

Add translation to existing namespace

  1. Add key to both locale files:
// public/locales/fi/common.json
{ "myKey": "Suomeksi" }

// public/locales/en/common.json
{ "myKey": "In English" }
  1. Use in component:
import { useTranslation } from '@shared-i18n/index';

const { t } = useTranslation('common');
return <span>{t('myKey')}</span>;

Create new namespace

  1. Add namespace to packages/shared-i18n/src/config.ts:
ns: ['common', 'profile', 'newNamespace'],
  1. Create files: public/locales/{fi,en}/newNamespace.json

  2. Use: const { t } = useTranslation('newNamespace');

Add new language

  1. Update packages/shared-i18n/src/types.ts:
export type SupportedLanguage = 'fi' | 'en' | 'sv';
export const SUPPORTED_LANGUAGES = ['fi', 'en', 'sv'] as const;
export const LANGUAGE_INFO = {
  fi: { code: 'fi', name: 'Finnish', nativeName: 'Suomi' },
  en: { code: 'en', name: 'English', nativeName: 'English' },
  sv: { code: 'sv', name: 'Swedish', nativeName: 'Svenska' },
};
  1. Update DB constraint in new migration:
ALTER TABLE public.profiles
DROP CONSTRAINT profiles_language_preference_check,
ADD CONSTRAINT profiles_language_preference_check
CHECK (language_preference IN ('fi', 'en', 'sv'));
  1. Create locale files: public/locales/sv/*.json

  2. Update dateLocale.ts with new locale import

Translation Patterns

Interpolation

{ "greeting": "Hello, {{name}}!" }
t('greeting', { name: 'John' })

Pluralization

{
  "item_one": "{{count}} item",
  "item_other": "{{count}} items"
}
t('item', { count: 5 })

Nested keys

{ "nav": { "home": "Home", "settings": "Settings" } }
t('nav.home')

Trans component (JSX interpolation)

import { Trans } from '@shared-i18n/index';

<Trans i18nKey="terms" t={t}>
  By continuing you agree to our <Link to="/terms">terms</Link>.
</Trans>

Migration Workflow

When migrating a component to i18n:

  1. Identify all user-visible strings
  2. Create translation keys (use descriptive, nested structure)
  3. Add to appropriate namespace (common for shared, feature-specific otherwise)
  4. Replace inline strings with t() calls
  5. Test both languages

Example migration:

// Before
<Button>Tallenna</Button>

// After
const { t } = useTranslation('common');
<Button>{t('common.save')}</Button>

Cross-cutting learnings: See .claude/LEARNINGS.md → "i18n/Translations" section for namespace patterns and useTranslation gotchas.

Admin Inline Editing (EditableText)

Any i18n-backed string rendered via t() can be opened for admin inline editing by wrapping it in <EditableText>. Non-admins see the original render with zero footprint; admins get a pencil that opens a FI/EN dialog, and Save commits to the locale JSON via the save-i18n-value Edge Function. No new keys are created — the key must already exist in the locale file.

Import: import { EditableText } from "@/components/inline-edit/EditableText";

Pattern 1 — Plain text (title, paragraph, span)

<EditableText i18nKey="myPage.hero.title">
  {t("myPage.hero.title")}
</EditableText>

<EditableText i18nKey="myPage.hero.body" placement="block">
  <p>{t("myPage.hero.body")}</p>
</EditableText>
  • placement="inline" (default) — pencil inline with the text (titles, spans).
  • placement="block" — pencil absolute-positioned top-right (paragraphs, lists, cards).
  • namespace defaults to "common"; other allowed: cinema, landing, profile, idea-machina. When adding a new namespace, also update ALLOWED_NAMESPACES in the Edge Function.

Pattern 2 — Lists (e.g. Bible refs, link lists)

EditableText edits one string (Textarea). For lists, store items newline-separated in a single i18n key and split in render — not comma-separated (Bible refs can contain commas themselves like Joh. 3:16, 17).

i18n value:

"verses": "Joh. 3:16\nRoom. 10:9–10\nEf. 2:8–9\nHepr. 11:1"

Render:

<EditableText i18nKey="...verses" placement="block">
  <ul className="list-disc list-inside space-y-1">
    {t("...verses")
      .split(/\r?\n/)
      .map((s) => s.trim())
      .filter(Boolean)
      .map((ref) => (
        <li key={ref}><BibleReferenceText text={ref} versionCode="finpr_finn" /></li>
      ))}
  </ul>
</EditableText>

Admin types one item per line in the dialog. .filter(Boolean) tolerates blank lines.

Reusable for: bullet lists, link lists, CTA text variants, any n-item list.

When NOT to use EditableText

  • Dynamic DB content — edits locale JSON, not Supabase tables. Use a DB-backed content system instead.
  • Per-user/per-tenant variants — same reason; locale JSON is global.
  • Rich text / Markdown — v1 is plain Textarea only.
  • Creating new keys inline — v1 requires the key to exist; returns 400 otherwise.

Full reference

Docs/ai/inline-content-editor.md — architecture diagram, Edge Function flow, GitHub commit details, v1 scope boundaries, v2+ expansion paths.

Files Reference

For detailed API and patterns, see references/i18n-api.md.