Class: Kotoshu::Cache::LanguageCache

Inherits:
BaseCache
  • Object
show all
Defined in:
lib/kotoshu/cache/language_cache.rb

Overview

Language cache for dynamic dictionary and grammar rule downloads.

Manages per-language dictionary and grammar rule downloads from a remote repository. Resources are cached locally in $XDG_CACHE_HOME/kotoshu/languages/{code}/ with metadata for versioning and expiration.

Extends BaseCache for common download, metadata, and validation logic.

Examples:

Getting a cached spelling dictionary

cache = LanguageCache.new
result = cache.get('en')
# => { aff_path: "~/.cache/kotoshu/languages/en/spelling/index.aff",
#      dic_path: "~/.cache/kotoshu/languages/en/spelling/index.dic",
#      metadata: { ... } }

Checking cache statistics

stats = cache.stats
# => { hits: 5, misses: 1, hit_rate: 0.83, ... }

Constant Summary collapse

RESOURCE_TYPES =

Supported resource types

%w[spelling grammar frequency].freeze
AVAILABLE_LANGUAGES =

Available languages (plan 107): every language staged in the dictionaries manifest, vendored offline-safe by Cache::StagedLanguages and refreshed by rake kotoshu:staged_languages:sync. Two tiers:

  • full feature — a language module under Kotoshu::Languages (registered keyboard + verified specimens); see full_feature_languages;
  • basic — manifest-present but module-less: the script fallback tokenizer (Language::Script) and the generic keyboard default still serve setup/check/suggest. Local files come via setup --aff/--dic instead.
StagedLanguages::CODES

Instance Attribute Summary

Attributes inherited from BaseCache

#cache_path, #cache_ttl, #github_url, #max_cache_size, #source_registry, #url_base

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from BaseCache

#available?, #cache_kind, #cached_data?, #clean, #clear, #clear_all, #download, #evict, #get, #initialize, #offline?, #read_metadata, #reset_stats, #stats

Constructor Details

This class inherits a constructor from Kotoshu::Cache::BaseCache

Class Method Details

.full_feature_languages ⇒ Array<String>

Languages on the full-feature tier: staged in the manifest AND wired as a gem module (Kotoshu::Languages) — registered keyboard layout plus engine-verified specimens. Everything else in #available_languages is the basic tier (plan 107).

Returns:

  • (Array<String>) —

    Sorted full-feature language codes



137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/kotoshu/cache/language_cache.rb', line 137

def self.full_feature_languages
  Language::Registry.supported_codes
    .map { |code| code.split("-").first }
    .uniq
    .select { |base| AVAILABLE_LANGUAGES.include?(base) }
    .concat(
      # Script-keyed staged dictionaries (zh-Hans-CN resolves to
      # the zh-Hans wordlist) surface under their staged names.
      AVAILABLE_LANGUAGES.select { |code| code.include?("-") }
    )
    .uniq
    .sort
end

Instance Method Details

#available_languages ⇒ Array<String>

Get list of available languages.

Returns:

  • (Array<String>) —

    List of supported language codes



127
128
129
# File 'lib/kotoshu/cache/language_cache.rb', line 127

def available_languages
  AVAILABLE_LANGUAGES.dup
end

#cache_size ⇒ Integer

Get cache size in bytes (override for language-specific tracking).

Returns:

  • (Integer) —

    Total size in bytes



183
184
185
186
187
188
189
# File 'lib/kotoshu/cache/language_cache.rb', line 183

def cache_size
  total = 0
  Dir.glob(File.join(@cache_path, "languages", "**", "*.dic")).each do |path|
    total += File.size(path) if File.file?(path)
  end
  total
end

#cached_resources ⇒ Array<String>

List all cached resources.

Returns:

  • (Array<String>) —

    List of cached resource identifiers



194
195
196
197
198
199
200
# File 'lib/kotoshu/cache/language_cache.rb', line 194

def cached_resources
  Dir.glob(File.join(@cache_path, "languages", "**", "metadata.json")).map do |path|
    relative = Pathname.new(path).relative_path_from(Pathname.new(@cache_path))
    parts = relative.to_s.split("/")
    "#{parts[1]}:#{parts[2]}"
  end.uniq
end

#frequency_available?(language_code) ⇒ Boolean

Check if frequency data is available for a language.

Parameters:

  • language_code (String) —

    ISO 639-1 language code

Returns:

  • (Boolean) —

    True if frequency data exists



119
120
121
122
# File 'lib/kotoshu/cache/language_cache.rb', line 119

def frequency_available?(language_code)
  resource_id = "#{language_code}:frequency"
  available?(resource_id)
end

#get_dictionary(language, force_download: false) ⇒ Hash

Alias for get_spelling for backward compatibility.

Parameters:

  • language (String) —

    Language code

  • force_download (Boolean) (defaults to: false) —

    Force re-download

Returns:

  • (Hash) —

    Dictionary paths and metadata



100
101
102
# File 'lib/kotoshu/cache/language_cache.rb', line 100

def get_dictionary(language, force_download: false)
  get_spelling(language, force_download)
end

#get_grammar(language, force_download: false) ⇒ Hash

Get or download grammar rules for a language.

Parameters:

  • language (String) —

    Language code

  • force_download (Boolean) (defaults to: false) —

    Force re-download

Returns:

  • (Hash) —

    Rules path and metadata



109
110
111
112
113
# File 'lib/kotoshu/cache/language_cache.rb', line 109

def get_grammar(language, force_download: false)
  resource_id = "#{language}:grammar"
  result = get(resource_id, force_download: force_download)
  result || download_grammar(language)
end

#get_spelling(language, force_download: false) ⇒ Hash

Get or download spelling dictionary for a language.

Parameters:

  • language (String) —

    Language code (e.g., 'en', 'de')

  • force_download (Boolean) (defaults to: false) —

    Force re-download even if cached

Returns:

  • (Hash) —

    Dictionary paths and metadata



47
48
49
50
51
# File 'lib/kotoshu/cache/language_cache.rb', line 47

def get_spelling(language, force_download: false)
  resource_id = "#{language}:spelling"
  result = get(resource_id, force_download: force_download)
  result || download_spelling(language)
end

#install_local(language, aff:, dic:, force: false) ⇒ Hash

Install a spelling dictionary from local files (no download). Used by ResourceManager.setup_from_local when the user already has .aff/.dic files on disk. Symlinks the source files into the cache directory so subsequent cache lookups find them. Existing symlinks are replaced when force: true; existing real files raise ArgumentError unless force: true.

Parameters:

  • language (String) —

    Language code

  • aff (String) —

    Path to .aff file

  • dic (String) —

    Path to .dic file

  • force (Boolean) (defaults to: false) —

    Overwrite existing install

Returns:

  • (Hash) —

    Installed paths



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/kotoshu/cache/language_cache.rb', line 65

def install_local(language, aff:, dic:, force: false)
  require "fileutils"

  resource_id = "#{language}:spelling"
  lang_path = resource_dir_for(resource_id)
  FileUtils.mkdir_p(lang_path)

  target_aff = File.join(lang_path, "index.aff")
  target_dic = File.join(lang_path, "index.dic")

  if File.exist?(target_aff) || File.symlink?(target_aff)
    raise ArgumentError, "#{target_aff} already exists (use force: true to overwrite)" unless force

    File.unlink(target_aff)
  end
  if File.exist?(target_dic) || File.symlink?(target_dic)
    raise ArgumentError, "#{target_dic} already exists (use force: true to overwrite)" unless force

    File.unlink(target_dic)
  end

  File.symlink(File.expand_path(aff), target_aff)
  File.symlink(File.expand_path(dic), target_dic)

  ((resource_id),
                 (language, "spelling", "local-source"))

  { aff_path: target_aff, dic_path: target_dic, source: :local }
end

#language_info(language_code) ⇒ Hash

Get language metadata (word count, license, source).

Parameters:

  • language_code (String) —

    The language code

Returns:

  • (Hash) —

    Language info



169
170
171
172
173
174
175
176
177
178
# File 'lib/kotoshu/cache/language_cache.rb', line 169

def language_info(language_code)
  {
    "de" => { name: "German", word_count: 75_873, license: "GPL", source: "igerman98" },
    "en" => { name: "English", word_count: 49_568, license: "LGPL/MPL/GPL", source: "SCOWL" },
    "es" => { name: "Spanish", word_count: 57_344, license: "GPL", source: "LibreOffice" },
    "fr" => { name: "French", word_count: 84_310, license: "MPL 2.0", source: "Grammalecte" },
    "pt" => { name: "Portuguese", word_count: 312_368, license: "LGPLv3 + MPL", source: "VERO" },
    "ru" => { name: "Russian", word_count: 146_269, license: "BSD-style", source: "Alexander Lebedev" }
  }[language_code] || { name: language_code.upcase, word_count: 0, license: "Unknown", source: "Unknown" }
end

#language_path(language, type) ⇒ String

Absolute on-disk path for a (language, resource_type) pair.

Composes cache_path/languages/{lang}/{type}. Use this rather than reaching into cache_path from outside the cache — it keeps the on-disk layout encapsulated and lets the layout evolve without breaking callers.

Parameters:

  • language (String) —

    Language code (e.g., 'en', 'de')

  • type (String) —

    Resource type ('spelling', 'grammar', 'frequency')

Returns:

  • (String) —

    Absolute directory path for the resource



161
162
163
# File 'lib/kotoshu/cache/language_cache.rb', line 161

def language_path(language, type)
  File.join(@cache_path, "languages", language, type)
end

#load_cached(resource_id) ⇒ Object?

Load cached resource data (implements abstract method).

Public — this is the cache-only reader BaseCache declares publicly; resolve paths use it to read without any download.

Parameters:

  • resource_id (String) —

    The resource identifier

Returns:

  • (Object, nil) —

    Loaded resource or nil



514
515
516
517
518
519
520
521
522
523
# File 'lib/kotoshu/cache/language_cache.rb', line 514

def load_cached(resource_id)
  parts = parse_resource_id(resource_id)
  return nil unless parts

  type = parts[1]
   = (resource_id)
  return nil unless 

  load_cached_resource_by_type(resource_id, type, )
end

#metadata_path_for(resource_id) ⇒ String

Get metadata file path for a resource.

Parameters:

  • resource_id (String) —

    The resource identifier

Returns:

  • (String) —

    Metadata file path



529
530
531
532
533
# File 'lib/kotoshu/cache/language_cache.rb', line 529

def (resource_id)
  language = extract_language(resource_id)
  type = extract_type(resource_id)
  File.join(language_path(language, type), "metadata.json")
end

#resource_dir_for(resource_id) ⇒ String

Get resource directory path.

Parameters:

  • resource_id (String) —

    The resource identifier

Returns:

  • (String) —

    Resource directory path



539
540
541
# File 'lib/kotoshu/cache/language_cache.rb', line 539

def resource_dir_for(resource_id)
  language_path(extract_language(resource_id), extract_type(resource_id))
end

#resource_files_exist?(resource_id) ⇒ Boolean

Check if all resource files exist.

Parameters:

  • resource_id (String) —

    The resource identifier

Returns:

  • (Boolean) —

    True if all files exist



547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
# File 'lib/kotoshu/cache/language_cache.rb', line 547

def resource_files_exist?(resource_id)
  type = extract_type(resource_id)
  return false unless type

  lang_path = resource_dir_for(resource_id)

  case type
  when "spelling"
    hunspell = File.exist?(File.join(lang_path, "index.aff")) &&
      File.exist?(File.join(lang_path, "index.dic"))
    hunspell || File.exist?(File.join(lang_path, "words.txt"))
  when "grammar"
    File.exist?(File.join(lang_path, "rules.yaml"))
  when "frequency"
    File.exist?(File.join(lang_path, "frequency.json"))
  else
    false
  end
end

#supports_resource?(resource_id) ⇒ Boolean

Check if a resource type is supported.

Parameters:

  • resource_id (String) —

    The resource identifier (e.g., "en:spelling")

Returns:

  • (Boolean) —

    True if supported



206
207
208
209
210
211
212
# File 'lib/kotoshu/cache/language_cache.rb', line 206

def supports_resource?(resource_id)
  parts = resource_id.split(":")
  return false unless parts.size == 2

  language, type = parts
  AVAILABLE_LANGUAGES.include?(language) && RESOURCE_TYPES.include?(type)
end